From ad9dd08a450ffd27d088cf86bbdd1051cfc57fad Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 22 Jan 2026 12:16:04 -0800 Subject: [PATCH 01/21] Add Starlight doc files Signed-off-by: Jeremy Parr-Pearson --- docs/.gitignore | 6 + docs/astro.config.mjs | 87 ++++ docs/public/CNAME.example | 1 + docs/public/favicon-32x32.png | Bin 0 -> 2346 bytes .../src/assets/valkey-logo-with-name-dark.svg | 57 +++ .../assets/valkey-logo-with-name-light.svg | 58 +++ docs/src/content.config.ts | 7 + docs/src/content/docs/appendix.md | 206 ++++++++ docs/src/content/docs/commons/migration.md | 114 +++++ docs/src/content/docs/commons/upgrade.md | 14 + docs/src/content/docs/index.mdx | 42 ++ docs/src/content/docs/observability.md | 96 ++++ docs/src/content/docs/overview.md | 24 + docs/src/content/docs/preface.md | 79 +++ docs/src/content/docs/redis.md | 42 ++ docs/src/content/docs/redis/cluster.md | 144 ++++++ .../content/docs/redis/connection-modes.md | 171 +++++++ docs/src/content/docs/redis/drivers.md | 183 +++++++ .../content/docs/redis/getting-started.mdx | 86 ++++ docs/src/content/docs/redis/hash-mappers.md | 109 ++++ docs/src/content/docs/redis/pipelining.md | 47 ++ docs/src/content/docs/redis/pubsub.mdx | 231 +++++++++ docs/src/content/docs/redis/redis-cache.md | 269 ++++++++++ .../docs/redis/redis-repositories/anatomy.md | 122 +++++ .../redis-repositories/cdi-integration.md | 65 +++ .../docs/redis/redis-repositories/cluster.md | 35 ++ .../redis/redis-repositories/expirations.md | 76 +++ .../docs/redis/redis-repositories/indexes.md | 160 ++++++ .../redis/redis-repositories/keyspaces.md | 57 +++ .../docs/redis/redis-repositories/mapping.md | 195 ++++++++ .../docs/redis/redis-repositories/queries.md | 74 +++ .../redis-repositories/query-by-example.md | 239 +++++++++ .../docs/redis/redis-repositories/usage.md | 158 ++++++ docs/src/content/docs/redis/redis-streams.md | 322 ++++++++++++ docs/src/content/docs/redis/scripting.mdx | 82 ++++ .../content/docs/redis/support-classes.mdx | 72 +++ docs/src/content/docs/redis/template.mdx | 341 +++++++++++++ docs/src/content/docs/redis/transactions.md | 117 +++++ docs/src/content/docs/repositories.md | 15 + .../docs/repositories/core-concepts.md | 130 +++++ .../docs/repositories/core-domain-events.md | 39 ++ .../docs/repositories/core-extensions.md | 8 + .../docs/repositories/create-instances.mdx | 115 +++++ .../repositories/custom-implementations.mdx | 464 ++++++++++++++++++ .../content/docs/repositories/definition.md | 163 ++++++ .../docs/repositories/null-handling.md | 95 ++++ .../docs/repositories/object-mapping.md | 429 ++++++++++++++++ .../content/docs/repositories/projections.md | 290 +++++++++++ .../repositories/query-keywords-reference.md | 84 ++++ .../repositories/query-methods-details.md | 459 +++++++++++++++++ .../query-return-types-reference.md | 45 ++ docs/src/styles/code-wrap.css | 6 + docs/src/styles/custom.css | 63 +++ docs/tsconfig.json | 5 + spring-data-valkey/pom.xml | 22 - 55 files changed, 6598 insertions(+), 22 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/astro.config.mjs create mode 100644 docs/public/CNAME.example create mode 100644 docs/public/favicon-32x32.png create mode 100644 docs/src/assets/valkey-logo-with-name-dark.svg create mode 100644 docs/src/assets/valkey-logo-with-name-light.svg create mode 100644 docs/src/content.config.ts create mode 100644 docs/src/content/docs/appendix.md create mode 100644 docs/src/content/docs/commons/migration.md create mode 100644 docs/src/content/docs/commons/upgrade.md create mode 100644 docs/src/content/docs/index.mdx create mode 100644 docs/src/content/docs/observability.md create mode 100644 docs/src/content/docs/overview.md create mode 100644 docs/src/content/docs/preface.md create mode 100644 docs/src/content/docs/redis.md create mode 100644 docs/src/content/docs/redis/cluster.md create mode 100644 docs/src/content/docs/redis/connection-modes.md create mode 100644 docs/src/content/docs/redis/drivers.md create mode 100644 docs/src/content/docs/redis/getting-started.mdx create mode 100644 docs/src/content/docs/redis/hash-mappers.md create mode 100644 docs/src/content/docs/redis/pipelining.md create mode 100644 docs/src/content/docs/redis/pubsub.mdx create mode 100644 docs/src/content/docs/redis/redis-cache.md create mode 100644 docs/src/content/docs/redis/redis-repositories/anatomy.md create mode 100644 docs/src/content/docs/redis/redis-repositories/cdi-integration.md create mode 100644 docs/src/content/docs/redis/redis-repositories/cluster.md create mode 100644 docs/src/content/docs/redis/redis-repositories/expirations.md create mode 100644 docs/src/content/docs/redis/redis-repositories/indexes.md create mode 100644 docs/src/content/docs/redis/redis-repositories/keyspaces.md create mode 100644 docs/src/content/docs/redis/redis-repositories/mapping.md create mode 100644 docs/src/content/docs/redis/redis-repositories/queries.md create mode 100644 docs/src/content/docs/redis/redis-repositories/query-by-example.md create mode 100644 docs/src/content/docs/redis/redis-repositories/usage.md create mode 100644 docs/src/content/docs/redis/redis-streams.md create mode 100644 docs/src/content/docs/redis/scripting.mdx create mode 100644 docs/src/content/docs/redis/support-classes.mdx create mode 100644 docs/src/content/docs/redis/template.mdx create mode 100644 docs/src/content/docs/redis/transactions.md create mode 100644 docs/src/content/docs/repositories.md create mode 100644 docs/src/content/docs/repositories/core-concepts.md create mode 100644 docs/src/content/docs/repositories/core-domain-events.md create mode 100644 docs/src/content/docs/repositories/core-extensions.md create mode 100644 docs/src/content/docs/repositories/create-instances.mdx create mode 100644 docs/src/content/docs/repositories/custom-implementations.mdx create mode 100644 docs/src/content/docs/repositories/definition.md create mode 100644 docs/src/content/docs/repositories/null-handling.md create mode 100644 docs/src/content/docs/repositories/object-mapping.md create mode 100644 docs/src/content/docs/repositories/projections.md create mode 100644 docs/src/content/docs/repositories/query-keywords-reference.md create mode 100644 docs/src/content/docs/repositories/query-methods-details.md create mode 100644 docs/src/content/docs/repositories/query-return-types-reference.md create mode 100644 docs/src/styles/code-wrap.css create mode 100644 docs/src/styles/custom.css create mode 100644 docs/tsconfig.json diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..30fef23e9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +.astro/ +dist/ +node_modules/ + +.env +.env.production diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 000000000..16e8d628c --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,87 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; + +// https://astro.build/config +export default defineConfig({ + site: 'https://spring.valkey.io', + integrations: [ + starlight({ + title: 'Spring Data Redis', + logo: { + light: './src/assets/valkey-logo-with-name-light.svg', + dark: './src/assets/valkey-logo-with-name-dark.svg', + replacesTitle: true, + }, + customCss: ['./src/styles/custom.css', './src/styles/code-wrap.css'], + favicon: '/favicon-32x32.png', + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/valkey-io/spring-data-valkey' } + ], + sidebar: [ + { + label: 'Overview', + items: [ + { label: 'Spring Data Redis', slug: 'overview' }, + { label: 'Upgrading Spring Data', slug: 'commons/upgrade' }, + { label: 'Migration Guides', slug: 'commons/migration' }, + ] + }, + { + label: 'Redis', + items: [ + { label: 'Redis Overview', slug: 'redis' }, + { label: 'Getting Started', slug: 'redis/getting-started' }, + { label: 'Drivers', slug: 'redis/drivers' }, + { label: 'Connection Modes', slug: 'redis/connection-modes' }, + { label: 'RedisTemplate', slug: 'redis/template' }, + { label: 'Redis Cache', slug: 'redis/redis-cache' }, + { label: 'Cluster', slug: 'redis/cluster' }, + { label: 'Hash Mapping', slug: 'redis/hash-mappers' }, + { label: 'Pub/Sub Messaging', slug: 'redis/pubsub' }, + { label: 'Redis Streams', slug: 'redis/redis-streams' }, + { label: 'Scripting', slug: 'redis/scripting' }, + { label: 'Redis Transactions', slug: 'redis/transactions' }, + { label: 'Pipelining', slug: 'redis/pipelining' }, + { label: 'Support Classes', slug: 'redis/support-classes' }, + ] + }, + { + label: 'Repositories', + items: [ + { label: 'Redis Repositories Overview', slug: 'repositories' }, + { label: 'Core concepts', slug: 'repositories/core-concepts' }, + { label: 'Defining Repository Interfaces', slug: 'repositories/definition' }, + { label: 'Creating Repository Instances', slug: 'repositories/create-instances' }, + { label: 'Usage', slug: 'redis/redis-repositories/usage' }, + { label: 'Object Mapping Fundamentals', slug: 'repositories/object-mapping' }, + { label: 'Object-to-Hash Mapping', slug: 'redis/redis-repositories/mapping' }, + { label: 'Keyspaces', slug: 'redis/redis-repositories/keyspaces' }, + { label: 'Secondary Indexes', slug: 'redis/redis-repositories/indexes' }, + { label: 'Time To Live', slug: 'redis/redis-repositories/expirations' }, + { label: 'Redis-specific Query Methods', slug: 'redis/redis-repositories/queries' }, + { label: 'Query by Example', slug: 'redis/redis-repositories/query-by-example' }, + { label: 'Redis Repositories Running on a Cluster', slug: 'redis/redis-repositories/cluster' }, + { label: 'Redis Repositories Anatomy', slug: 'redis/redis-repositories/anatomy' }, + { label: 'Projections', slug: 'repositories/projections' }, + { label: 'Custom Repository Implementations', slug: 'repositories/custom-implementations' }, + { label: 'Publishing Events from Aggregate Roots', slug: 'repositories/core-domain-events' }, + { label: 'Null Handling of Repository Methods', slug: 'repositories/null-handling' }, + { label: 'CDI Integration', slug: 'redis/redis-repositories/cdi-integration' }, + { label: 'Repository query keywords', slug: 'repositories/query-keywords-reference' }, + { label: 'Repository query return types', slug: 'repositories/query-return-types-reference' }, + ] + }, + { label: 'Observability', slug: 'observability' }, + { label: 'Appendix', slug: 'appendix' }, + { + label: 'External Links', + items: [ + { label: 'Javadoc', link: '/api/java/index.html' }, + { label: 'Wiki', link: 'https://github.com/spring-projects/spring-data-commons/wiki' }, + ] + }, + ], + }), + ], +}); diff --git a/docs/public/CNAME.example b/docs/public/CNAME.example new file mode 100644 index 000000000..4d809082e --- /dev/null +++ b/docs/public/CNAME.example @@ -0,0 +1 @@ +spring.valkey.io diff --git a/docs/public/favicon-32x32.png b/docs/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..e5729f5df37352d9c605317ec10b845dfc938d1e GIT binary patch literal 2346 zcmV+_3Dx$AP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rj1qBi%Fc@{)SO5SA%Sl8*R9M56mw8N9XBNhP z@BKCeR1{G}tcp6QC|Vb+AW|K$t=bXoB%-Z4X_LXKnI)PyOJ~x=aWYohIB7bdcC@az z){eGXwXT4O3t9mccR)c9L1q8G@7^@X5-1`oMC9R%P)Z+H%7(oeay0$A)H_?{^+-fki%761Zc9@w!5eHO@Hr6E z(x;}*$&s|{B&DQtA*TReZwE8t!dZ|IMQFe7w36}+U`fNXnU8t__yiys7~C|1TQ|6v zQ$SKm8b{LesIGOQH3Sk7`UZF9!}s1`_S8`XboG4^iQB+J;7C&vss;EcU`tbQ5#dhZ zV>awP%dSJ0C@rr-Qwl){=;}*Nt&{2+Cn&IJ8e@jN#-jJ5iHi)y%VB#FiTOafheT5X zfxvdmE(hRoMK!yAyu^k*XSrGM2n84dT{?AOMr=60nK_m_g^&4adn#GEg%~1;fUmcM zsWC%XIA=WJp}o+wW@R}G%mNCOQm6)S4OkBR1)#Rh#nCfnozG|I<8~VWIP6wpA_lN< z_IM&khG4f@04OP|Wb1+ReDm!&3X3aHN@0l5t9xhWP9MYk84(0^@7$Eczk#KI+e2V9 za6s3Mz^to@pb@|1Mxtr5)rL|(c%j^1r$_L?uSOHpy)z=ht^6{+-jl(;qgM#&?a$)(qls;(dA-xkiOidPwJnv5 z%Xe|>1_;>gRz6uUncvNe@nm5mkZ1)40`CAksjQ{!Ni`Nt;q7H-MqD_F2~iB`ABd(Y z9+g+I<6ssW_hxdd;1LKEKYB{`jUuMK8OF!6qX`cU;_c|cM1=QY*P%-!r=*czTtQvE zi=xt}P0J7mgjpM!u8;XkD3ns@xkPt;>RUIpmkDvh6ycvUlI1d49%m2k@vtYMb z&~<}TjwJ6!fjTrKgQKt#jIR^j8o@tvwnF3 z!GV4_9M6UJmiN~m~NLpyA!2J~#pOVbpFAq?yrK*H2fDBwRSX-rF8%kTfOmy)td#=YK; zDKSGZM9?%vJ5?}H1AN=#DIyH$6F?9Dj@)}tL2^nOS8qRL_m7wP=}aB~!w2=kqG|03 z*H~$d7Gm?#a=3L25$rY#UJe_EAy_RM9lXpd*X7oMSCC%J=BolawWkc%?vzkaR8C0m zZv1WGTcmt{kr9J~iHi!w?bgZsIUn74B?2xh<|Ok{0)`V;elbB5EKy;h3qS|^p&^;j$#e@&ds>Sgit4fdzr zqKR$x^Uk*bIX(q2C0t&(g2J)|^@3Q%`xlD}-!)DdEng58)pYvO5 z^J3tL703e)1Btx@{TMp1Czr0?$LZ4f;bbm;9lc2UbQYSXuvsn7MX}qhESeL|gfRo? z+p8;J%JYtcS!~*u$-M{VXiDKUjFvo(1KHMw^lt)YhKBSA*u3%s*6ljOw*41)SoRE; z+dvRn`;5-MUd)|72Bj30)%6@amCL%FXSjU*0lHx-p_ju-Y-A`iClB`=dxEKkPR~Vs z4%iGV2@dq*?~C3hc3dc{{(B6IMSE#SE2VI{+@xpSC3#0Wr_SA^uHM}^qKz2Ti{H+Q zWJ*jJ9eo_0V>^IU0IM=}UkBCz??*vV zIqP<1u>W{A70+t%v`0v=Kl5iqkT7KwJ-T&l$~g;I1-PKO3f{*YlePf;0lIbZp<5T9 zMh8Q{5Wx`P%Pprlef}0jrOz72FaZB9zWn;F5q$LCSO)Y7XsHQ>4H$Pk*?ZAgJ{d^D zT*aE%>GQYvWX%s`-zaW8qm)U^*W1qIs6iwqL@|2kYgjGM-2;WdVqm`q99myYB7rY} zvCl<$Si!oTXV`Y&B4rg-IBZrThWBAXLKLy%LK}6VgKFZf!MtA|gp564Kgg))o<)h(wFXMiIF$BDyECVHo0c zxurWXu(PD9{7&C3HntbYx+4WjbwdWNBu305UK#G%YYSEig4yF*rIiH##&pD=;uR zFfeW;5*z>k09SfcSaechcOY6Cgx@G{a;ABePT>%h=S&#LUDT#0SfONT5nC0O}VJbn-$q Ql>h($07*qoM6N<$g0FWc%m4rY literal 0 HcmV?d00001 diff --git a/docs/src/assets/valkey-logo-with-name-dark.svg b/docs/src/assets/valkey-logo-with-name-dark.svg new file mode 100644 index 000000000..27363693e --- /dev/null +++ b/docs/src/assets/valkey-logo-with-name-dark.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/assets/valkey-logo-with-name-light.svg b/docs/src/assets/valkey-logo-with-name-light.svg new file mode 100644 index 000000000..f84760803 --- /dev/null +++ b/docs/src/assets/valkey-logo-with-name-light.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 000000000..d9ee8c9d1 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/docs/src/content/docs/appendix.md b/docs/src/content/docs/appendix.md new file mode 100644 index 000000000..c233c9401 --- /dev/null +++ b/docs/src/content/docs/appendix.md @@ -0,0 +1,206 @@ +--- +title: Appendix +description: Additional reference information +--- + +## Schema + +[Spring Data Redis Schema (redis-namespace)](https://www.springframework.org/schema/redis/spring-redis-1.0.xsd) + +## Supported Commands + +*Table 1. Redis commands supported by `RedisTemplate`* + +| Command | Template Support | +|---------|------------------| +| APPEND | X | +| AUTH | X | +| BGREWRITEAOF | X | +| BGSAVE | X | +| BITCOUNT | X | +| BITFIELD | X | +| BITOP | X | +| BLPOP | X | +| BRPOP | X | +| BRPOPLPUSH | X | +| CLIENT KILL | X | +| CLIENT GETNAME | X | +| CLIENT LIST | X | +| CLIENT SETNAME | X | +| CLUSTER SLOTS | - | +| COMMAND | - | +| COMMAND COUNT | - | +| COMMAND GETKEYS | - | +| COMMAND INFO | - | +| CONFIG GET | X | +| CONFIG RESETSTAT | X | +| CONFIG REWRITE | - | +| CONFIG SET | X | +| DBSIZE | X | +| DEBUG OBJECT | - | +| DEBUG SEGFAULT | - | +| DECR | X | +| DECRBY | X | +| DEL | X | +| DISCARD | X | +| DUMP | X | +| ECHO | X | +| EVAL | X | +| EVALSHA | X | +| EXEC | X | +| EXISTS | X | +| EXPIRE | X | +| EXPIREAT | X | +| FLUSHALL | X | +| FLUSHDB | X | +| GEOADD | X | +| GEODIST | X | +| GEOHASH | X | +| GEOPOS | X | +| GEORADIUS | X | +| GEORADIUSBYMEMBER | X | +| GEOSEARCH | X | +| GEOSEARCHSTORE | X | +| GET | X | +| GETBIT | X | +| GETRANGE | X | +| GETSET | X | +| HDEL | X | +| HEXISTS | X | +| HEXPIRE | X | +| HEXPIREAT | X | +| HPEXPIRE | X | +| HPEXPIREAT | X | +| HPERSIST | X | +| HTTL | X | +| HPTTL | X | +| HGET | X | +| HGETALL | X | +| HINCRBY | X | +| HINCRBYFLOAT | X | +| HKEYS | X | +| HLEN | X | +| HMGET | X | +| HMSET | X | +| HSCAN | X | +| HSET | X | +| HSETNX | X | +| HVALS | X | +| INCR | X | +| INCRBY | X | +| INCRBYFLOAT | X | +| INFO | X | +| KEYS | X | +| LASTSAVE | X | +| LINDEX | X | +| LINSERT | X | +| LLEN | X | +| LPOP | X | +| LPUSH | X | +| LPUSHX | X | +| LRANGE | X | +| LREM | X | +| LSET | X | +| LTRIM | X | +| MGET | X | +| MIGRATE | - | +| MONITOR | - | +| MOVE | X | +| MSET | X | +| MSETNX | X | +| MULTI | X | +| OBJECT | - | +| PERSIST | X | +| PEXIPRE | X | +| PEXPIREAT | X | +| PFADD | X | +| PFCOUNT | X | +| PFMERGE | X | +| PING | X | +| PSETEX | X | +| PSUBSCRIBE | X | +| PTTL | X | +| PUBLISH | X | +| PUBSUB | - | +| PUBSUBSCRIBE | - | +| QUIT | X | +| RANDOMKEY | X | +| RENAME | X | +| RENAMENX | X | +| REPLICAOF | X | +| RESTORE | X | +| ROLE | - | +| RPOP | X | +| RPOPLPUSH | X | +| RPUSH | X | +| RPUSHX | X | +| SADD | X | +| SAVE | X | +| SCAN | X | +| SCARD | X | +| SCRIPT EXITS | X | +| SCRIPT FLUSH | X | +| SCRIPT KILL | X | +| SCRIPT LOAD | X | +| SDIFF | X | +| SDIFFSTORE | X | +| SELECT | X | +| SENTINEL FAILOVER | X | +| SENTINEL GET-MASTER-ADD-BY-NAME | - | +| SENTINEL MASTER | - | +| SENTINEL MASTERS | X | +| SENTINEL MONITOR | X | +| SENTINEL REMOVE | X | +| SENTINEL RESET | - | +| SENTINEL SET | - | +| SENTINEL SLAVES | X | +| SET | X | +| SETBIT | X | +| SETEX | X | +| SETNX | X | +| SETRANGE | X | +| SHUTDOWN | X | +| SINTER | X | +| SINTERSTORE | X | +| SISMEMBER | X | +| SLAVEOF | X | +| SLOWLOG | - | +| SMEMBERS | X | +| SMOVE | X | +| SORT | X | +| SPOP | X | +| SRANDMEMBER | X | +| SREM | X | +| SSCAN | X | +| STRLEN | X | +| SUBSCRIBE | X | +| SUNION | X | +| SUNIONSTORE | X | +| SYNC | - | +| TIME | X | +| TTL | X | +| TYPE | X | +| UNSUBSCRIBE | X | +| UNWATCH | X | +| WATCH | X | +| ZADD | X | +| ZCARD | X | +| ZCOUNT | X | +| ZINCRBY | X | +| ZINTERSTORE | X | +| ZLEXCOUNT | - | +| ZRANGE | X | +| ZRANGEBYLEX | - | +| ZREVRANGEBYLEX | - | +| ZRANGEBYSCORE | X | +| ZRANGESTORE | X | +| ZRANK | X | +| ZREM | X | +| ZREMRANGEBYLEX | - | +| ZREMRANGEBYRANK | X | +| ZREVRANGE | X | +| ZREVRANGEBYSCORE | X | +| ZREVRANK | X | +| ZSCAN | X | +| ZSCORE | X | +| ZUNINONSTORE | X | diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md new file mode 100644 index 000000000..ec4ffd6ec --- /dev/null +++ b/docs/src/content/docs/commons/migration.md @@ -0,0 +1,114 @@ +--- +title: Migration Guides +description: Migration steps, deprecations, and removals +--- + +This section contains details about migration steps, deprecations, and removals. + +## Upgrading from 2.x to 3.x + +### Re-/moved Types + +| Type | Replacement | +|------|-------------| +| o.s.d.redis.Version | o.s.d.util.Version | +| o.s.d.redis.VersionParser | - | +| o.s.d.redis.connection.RedisZSetCommands.Aggregate | o.s.d.redis.connection.zset.Aggregate | +| o.s.d.redis.connection.RedisZSetCommands.Tuple | o.s.d.redis.connection.zset.Tuple | +| o.s.d.redis.connection.RedisZSetCommands.Weights | o.s.d.redis.connection.zset.Weights | +| o.s.d.redis.connection.RedisZSetCommands.Range | o.s.d.domain.Range | +| o.s.d.redis.connection.RedisZSetCommands.Limit | o.s.d.redis.connection.Limit.java | +| o.s.d.redis.connection.jedis.JedisUtils | - | +| o.s.d.redis.connection.jedis.JedisVersionUtil | - | +| o.s.d.redis.core.convert.CustomConversions | o.s.d.convert.CustomConversions | + +### Changed Methods and Types + +*Table 1. Core* + +| Type | Method | Replacement | +|------|--------|-------------| +| o.s.d.redis.core.Cursor | open | - | +| o.s.d.redis.core.RedisTemplate | execute | doWithKeys | +| o.s.d.redis.stream.StreamMessageListenerContainer | isAutoAck | isAutoAcknowledge | +| o.s.d.redis.stream.StreamMessageListenerContainer | autoAck | autoAcknowledge | + +*Table 2. Redis Connection* + +| Type | Method | Replacement | +|------|--------|-------------| +| o.s.d.redis.connection.ClusterCommandExecutionFailureException | getCauses | getSuppressed | +| o.s.d.redis.connection.RedisConnection | bgWriteAof | bgReWriteAof | +| o.s.d.redis.connection.RedisConnection | slaveOf | replicaOf | +| o.s.d.redis.connection.RedisConnection | slaveOfNoOne | replicaOfNoOne | +| o.s.d.redis.connection.ReactiveClusterCommands | clusterGetSlaves | clusterGetReplicas | +| o.s.d.redis.connection.ReactiveClusterCommands | clusterGetMasterSlaveMap | clusterGetMasterReplicaMap | +| o.s.d.redis.connection.ReactiveKeyCommands | getNewName | getNewKey | +| o.s.d.redis.connection.RedisClusterNode.Flag | SLAVE | REPLICA | +| o.s.d.redis.connection.RedisClusterNode.Builder | slaveOf | replicaOf | +| o.s.d.redis.connection.RedisNode | isSlave | isReplica | +| o.s.d.redis.connection.RedisSentinelCommands | slaves | replicas | +| o.s.d.redis.connection.RedisServer | getNumberSlaves | getNumberReplicas | +| o.s.d.redis.connection.RedisServerCommands | slaveOf | replicaOf | +| o.s.d.redis.core.ClusterOperations | getSlaves | getReplicas | +| o.s.d.redis.core.RedisOperations | slaveOf | replicaOf | + +*Table 3. Redis Operations* + +| Type | Method | Replacement | +|------|--------|-------------| +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoAdd | add | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoDist | distance | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoHash | hash | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoPos | position | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRadius | radius | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRadiusByMember | radius | +| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRemove | remove | + +*Table 4. Redis Cache* + +| Type | Method | Replacement | +|------|--------|-------------| +| o.s.d.redis.cache.RedisCacheConfiguration | prefixKeysWith | prefixCacheNameWith | +| o.s.d.redis.cache.RedisCacheConfiguration | getKeyPrefix | getKeyPrefixFor | + +### Jedis + +Please read the Jedis [upgrading guide](https://github.com/redis/jedis/blob/v4.0.0/docs/3to4.md) which covers important driver changes. + +*Table 5. Jedis Redis Connection* + +| Type | Method | Replacement | +|------|--------|-------------| +| o.s.d.redis.connection.jedis.JedisConnectionFactory | getShardInfo | _can be obtained via JedisClientConfiguration_ | +| o.s.d.redis.connection.jedis.JedisConnectionFactory | setShardInfo | _can be set via JedisClientConfiguration_ | +| o.s.d.redis.connection.jedis.JedisConnectionFactory | createCluster | _now requires a `Connection` instead of `Jedis` instance_ | +| o.s.d.redis.connection.jedis.JedisConverters | | has package visibility now | +| o.s.d.redis.connection.jedis.JedisConverters | tuplesToTuples | - | +| o.s.d.redis.connection.jedis.JedisConverters | stringListToByteList | - | +| o.s.d.redis.connection.jedis.JedisConverters | stringSetToByteSet | - | +| o.s.d.redis.connection.jedis.JedisConverters | stringMapToByteMap | - | +| o.s.d.redis.connection.jedis.JedisConverters | tupleSetToTupleSet | - | +| o.s.d.redis.connection.jedis.JedisConverters | toTupleSet | - | +| o.s.d.redis.connection.jedis.JedisConverters | toDataAccessException | o.s.d.redis.connection.jedis.JedisExceptionConverter#convert | + +#### Transactions / Pipelining + +Pipelining and Transactions are now mutually exclusive. +The usage of server or connection commands in pipeline/transactions mode is no longer possible. + +### Lettuce + +#### Lettuce Pool + +`LettucePool` and its implementation `DefaultLettucePool` have been removed without replacement. +Please refer to the [driver documentation](https://lettuce.io/core/release/reference/index.html#_connection_pooling) for driver native pooling capabilities. +Methods accepting pooling parameters have been updated. +This effects methods on `LettuceConnectionFactory` and `LettuceConnection`. + +#### Lettuce Authentication + +`AuthenticatingRedisClient` has been removed without replacement. +Please refer to the [driver documentation](https://lettuce.io/core/release/reference/index.html#basic.redisuri) for `RedisURI` to set authentication data. + + diff --git a/docs/src/content/docs/commons/upgrade.md b/docs/src/content/docs/commons/upgrade.md new file mode 100644 index 000000000..422ac9c1b --- /dev/null +++ b/docs/src/content/docs/commons/upgrade.md @@ -0,0 +1,14 @@ +--- +title: Upgrading Spring Data +description: Instructions for upgrading Spring Data versions +--- + +Instructions for how to upgrade from earlier versions of Spring Data are provided on the project [wiki](https://github.com/spring-projects/spring-data-commons/wiki). +Follow the links in the [release notes section](https://github.com/spring-projects/spring-data-commons/wiki#release-notes) to find the version that you want to upgrade to. + +Upgrading instructions are always the first item in the release notes. If you are more than one release behind, please make sure that you also review the release notes of the versions that you jumped. + +Once you've decided to upgrade your application, you can find detailed information regarding specific features in the rest of the document. +You can find [migration guides](/commons/migration/) specific to major version migrations at the end of this document. + +Spring Data's documentation is specific to that version, so any information that you find in here will contain the most up-to-date changes that are in that version. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 000000000..c4f1f655a --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,42 @@ +--- +title: Spring Data Redis +description: Spring Data Redis provides Redis connectivity and repository support for the Redis database. It eases development of applications with a consistent programming model that need to access Redis data sources. +template: splash +hero: + tagline: Redis connectivity and repository support for Spring applications + actions: + - text: Overview + link: /overview/ + icon: right-arrow + variant: primary + - text: View on GitHub + link: https://github.com/valkey-io/spring-data-valkey + icon: external +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; + +## Quick Navigation + + + + Redis connectivity, templates, and operations + + [Learn more →](/redis/) + + + Repository abstraction for Redis data access + + [Learn more →](/repositories/) + + + Monitoring and observability integration + + [Learn more →](/observability/) + + + Upgrade from Spring Data Redis + + [Learn more →](/commons/migration/) + + diff --git a/docs/src/content/docs/observability.md b/docs/src/content/docs/observability.md new file mode 100644 index 000000000..b847ee462 --- /dev/null +++ b/docs/src/content/docs/observability.md @@ -0,0 +1,96 @@ +--- +title: Observability +description: Observability Integration for monitoring and metrics +--- + +Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. +Spring Data Redis ships with a Micrometer integration through the Lettuce driver to collect observations during Redis interaction. +Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Redis command. + +To enable the integration, apply the following configuration to `LettuceClientConfiguration`: + +```java +@Configuration +class ObservabilityConfiguration { + + @Bean + public ClientResources clientResources(ObservationRegistry observationRegistry) { + + return ClientResources.builder() + .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) + .build(); + } + + @Bean + public LettuceConnectionFactory lettuceConnectionFactory(ClientResources clientResources) { + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .clientResources(clientResources).build(); + RedisConfiguration redisConfiguration = …; + return new LettuceConnectionFactory(redisConfiguration, clientConfig); + } +} +``` + +See also [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis) for further reference. + +## Observability - Metrics + +Below you can find a list of all metrics declared by this project. + +### Redis Command Observation + +> Timer created around a Redis command execution. + +**Metric name** `spring.data.redis`. **Type** `timer` and **base unit** `seconds`. + +Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. + +*Table 1. Low cardinality Keys* + +| Name | Description | +|------|-------------| +| `db.operation` | Redis command value. | +| `db.redis.database_index` | Redis database index. | +| `db.system` | Database system. | +| `db.user` | Redis user. | +| `net.peer.name` | Name of the database host. | +| `net.peer.port` | Logical remote port number. | +| `net.sock.peer.addr` | Mongo peer address. | +| `net.sock.peer.port` | Mongo peer port. | +| `net.transport` | Network transport. | + +*Table 2. High cardinality Keys* + +| Name | Description | +|------|-------------| +| `db.statement` | Redis statement. | +| `spring.data.redis.command.error` | Redis error response. | + +## Observability - Spans + +Below you can find a list of all spans declared by this project. + +### Redis Command Observation Span + +> Timer created around a Redis command execution. + +**Span name** `spring.data.redis`. + +Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. + +*Table 3. Tag Keys* + +| Name | Description | +|------|-------------| +| `db.operation` | Redis command value. | +| `db.redis.database_index` | Redis database index. | +| `db.statement` | Redis statement. | +| `db.system` | Database system. | +| `db.user` | Redis user. | +| `net.peer.name` | Name of the database host. | +| `net.peer.port` | Logical remote port number. | +| `net.sock.peer.addr` | Mongo peer address. | +| `net.sock.peer.port` | Mongo peer port. | +| `net.transport` | Network transport. | +| `spring.data.redis.command.error` | Redis error response. | diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md new file mode 100644 index 000000000..a1a0e2c15 --- /dev/null +++ b/docs/src/content/docs/overview.md @@ -0,0 +1,24 @@ +--- +title: Spring Data Redis +description: Spring Data Redis provides Redis connectivity and repository support for the Redis database. +--- + + +_Spring Data Redis provides Redis connectivity and repository support for the Redis database. It eases development of applications with a consistent programming model that need to access Redis data sources._ + +| Section | Description | +|---------|-------------| +| [Redis](../redis) | Redis support and connectivity | +| [Redis Repositories](../repositories) | Redis Repositories | +| [Observability](../observability) | Observability Integration | +| [Wiki](https://github.com/spring-projects/spring-data-commons/wiki) | What's New, Upgrade Notes, Supported Versions, additional cross-version information. | + +## Authors + +Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch, Jay Bryant + +## Copyright + +(C) 2008-2024 VMware, Inc. + +Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/docs/src/content/docs/preface.md b/docs/src/content/docs/preface.md new file mode 100644 index 000000000..cf697549c --- /dev/null +++ b/docs/src/content/docs/preface.md @@ -0,0 +1,79 @@ +--- +title: Preface +description: Introduction to Spring Data Redis +--- + +The Spring Data Redis project applies core Spring concepts to the development of solutions by using a key-value style data store. +We provide a "template" as a high-level abstraction for sending and receiving messages. +You may notice similarities to the JDBC support in the Spring Framework. + +This section provides an easy-to-follow guide for getting started with the Spring Data Redis module. + +## Learning Spring + +Spring Data uses Spring framework's [core](https://docs.spring.io/spring-framework/reference/core.html) functionality, including: + +* [IoC](https://docs.spring.io/spring-framework/reference/core.html#beans) container +* [type conversion system](https://docs.spring.io/spring-framework/reference/core.html#validation) +* [expression language](https://docs.spring.io/spring-framework/reference/core.html#expressions) +* [JMX integration](https://docs.spring.io/spring-framework/reference/integration.html#jmx) +* [DAO exception hierarchy](https://docs.spring.io/spring-framework/reference/data-access.html#dao-exceptions) + +While you need not know the Spring APIs, understanding the concepts behind them is important. +At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. + +The core functionality of the Redis support can be used directly, with no need to invoke the IoC services of the Spring Container. +This is much like `JdbcTemplate`, which can be used "standalone" without any other services of the Spring container. +To leverage all the features of Spring Data Redis, such as the repository support, you need to configure some parts of the library to use Spring. + +To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. +There are a lot of articles, blog entries, and books on the subject. +See the Spring framework [home page](https://spring.io/projects/spring-framework/) for more information. + +In general, this should be the starting point for developers wanting to try Spring Data Redis. + +## Learning NoSQL and Key Value Stores + +NoSQL stores have taken the storage world by storm. +It is a vast domain with a plethora of solutions, terms, and patterns (to make things worse, even the term itself has multiple [meanings](https://www.google.com/search?q=nosoql+acronym)). +While some of the principles are common, it is crucial that you be familiar to some degree with the stores supported by SDR. The best way to get acquainted with these solutions is to read their documentation and follow their examples. +It usually does not take more then five to ten minutes to go through them and, if you come from an RDMBS-only background, many times these exercises can be eye-openers. + +### Trying out the Samples + +One can find various samples for key-value stores in the dedicated Spring Data example repo, at [https://github.com/spring-projects/spring-data-examples/tree/main/redis](https://github.com/spring-projects/spring-data-examples/tree/main/redis). + +## Requirements + +Spring Data Redis binaries require JDK level 17 and above and [Spring Framework](https://spring.io/projects/spring-framework/) 6.0 and above. + +In terms of key-value stores, [Redis](https://redis.io) 2.6.x or higher is required. +Spring Data Redis is currently tested against the latest 6.0 release. + +## Additional Help Resources + +Learning a new framework is not always straightforward. +In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data Redis module. +However, if you encounter issues or you need advice, feel free to use one of the following links: + +### Community Forum + +Spring Data on [Stack Overflow](https://stackoverflow.com/questions/tagged/spring-data) is a tag for all Spring Data (not just Document) users to share information and help each other. +Note that registration is needed only for posting. + +### Professional Support + +Professional, from-the-source support, with guaranteed response time, is available from [Pivotal Software, Inc.](https://pivotal.io/), the company behind Spring Data and Spring. + +## Following Development + +For information on the Spring Data source code repository, nightly builds, and snapshot artifacts, see the Spring Data home [page](https://spring.io/projects/spring-data/). + +You can help make Spring Data best serve the needs of the Spring community by interacting with developers on Stack Overflow at either +[spring-data](https://stackoverflow.com/questions/tagged/spring-data) or [spring-data-redis](https://stackoverflow.com/questions/tagged/spring-data-redis). + +If you encounter a bug or want to suggest an improvement (including to this documentation), please create a ticket on [Github](https://github.com/spring-projects/spring-data-redis/issues/new). + +To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community [Portal](https://spring.io/). + +Lastly, you can follow the Spring [blog](https://spring.io/blog/) or the project team ([@SpringData](https://twitter.com/SpringData)) on Twitter. diff --git a/docs/src/content/docs/redis.md b/docs/src/content/docs/redis.md new file mode 100644 index 000000000..3cbbfec99 --- /dev/null +++ b/docs/src/content/docs/redis.md @@ -0,0 +1,42 @@ +--- +title: Redis Overview +description: Spring Data Redis support and connectivity +--- + +One of the key-value stores supported by Spring Data is [Redis](https://redis.io). +To quote the Redis project home page: + +> Redis is an advanced key-value store. +> It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. +> All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. +> Redis supports different kind of sorting abilities. + +Spring Data Redis provides easy configuration and access to Redis from Spring applications. +It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns. + +Spring Data support for Redis contains a wide range of features: + +* [`RedisTemplate` and `ReactiveRedisTemplate` helper class](/redis/template) that increases productivity when performing common Redis operations. +Includes integrated serialization between objects and values. +* Exception translation into Spring's portable Data Access Exception hierarchy. +* Automatic implementation of [Repository interfaces](/repositories), including support for custom query methods. +* Feature-rich [Object Mapping](/redis/redis-repositories/mapping) integrated with Spring's Conversion Service. +* Annotation-based mapping metadata that is extensible to support other metadata formats. +* [Transactions](/redis/transactions) and [Pipelining](/redis/pipelining). +* [Redis Cache](/redis/redis-cache) integration through Spring's Cache abstraction. +* [Redis Pub/Sub Messaging](/redis/pubsub) and [Redis Stream](/redis/redis-streams) Listeners. +* [Redis Collection Implementations](/redis/support-classes) for Java such as `RedisList` or `RedisSet`. + +## Why Spring Data Redis? + +The Spring Framework is the leading full-stack Java/JEE application framework. +It provides a lightweight container and a non-invasive programming model enabled by the use of dependency injection, AOP, and portable service abstractions. + +[NoSQL](https://en.wikipedia.org/wiki/NoSQL) storage systems provide an alternative to classical RDBMS for horizontal scalability and speed. +In terms of implementation, key-value stores represent one of the largest (and oldest) members in the NoSQL space. + +The Spring Data Redis (SDR) framework makes it easy to write Spring applications that use the Redis key-value store by eliminating the redundant tasks and boilerplate code required for interacting with the store through Spring's excellent infrastructure support. + +## Redis Support High-level View + +The Redis support provides several components.For most tasks, the high-level abstractions and support services are the best choice.Note that, at any point, you can move between layers.For example, you can get a low-level connection (or even the native library) to communicate directly with Redis. diff --git a/docs/src/content/docs/redis/cluster.md b/docs/src/content/docs/redis/cluster.md new file mode 100644 index 000000000..9634f5100 --- /dev/null +++ b/docs/src/content/docs/redis/cluster.md @@ -0,0 +1,144 @@ +--- +title: Redis Cluster +description: Redis Cluster documentation +--- + +Working with [Redis Cluster](https://redis.io/topics/cluster-spec) requires Redis Server version 3.0+. +See the [Cluster Tutorial](https://redis.io/topics/cluster-tutorial) for more information. + +:::note +When using [Redis Repositories](/repositories) with Redis Cluster, make yourself familiar with how to [run Redis Repositories on a Cluster](/redis/redis-repositories/cluster). +::: + +:::caution +Do not rely on keyspace events when using Redis Cluster as keyspace events are not replicated across shards. +::: + +Pub/Sub [subscribes to a random cluster node](https://github.com/spring-projects/spring-data-redis/issues/1111) which only receives keyspace events from a single shard. +Use single-node Redis to avoid keyspace event loss. + +## Working With Redis Cluster Connection + +Redis Cluster behaves differently from single-node Redis or even a Sentinel-monitored master-replica environment. +This is because the automatic sharding maps a key to one of `16384` slots, which are distributed across the nodes. +Therefore, commands that involve more than one key must assert all keys map to the exact same slot to avoid cross-slot errors. +A single cluster node serves only a dedicated set of keys. +Commands issued against one particular server return results only for those keys served by that server. +As a simple example, consider the `KEYS` command. +When issued to a server in a cluster environment, it returns only the keys served by the node the request is sent to and not necessarily all keys within the cluster. +So, to get all keys in a cluster environment, you must read the keys from all the known master nodes. + +While redirects for specific keys to the corresponding slot-serving node are handled by the driver libraries, higher-level functions, such as collecting information across nodes or sending commands to all nodes in the cluster, are covered by `RedisClusterConnection`. +Picking up the keys example from earlier, this means that the `keys(pattern)` method picks up every master node in the cluster and simultaneously runs the `KEYS` command on every master node while picking up the results and returning the cumulated set of keys. +To just request the keys of a single node `RedisClusterConnection` provides overloads for those methods (for example, `keys(node, pattern)`). + +A `RedisClusterNode` can be obtained from `RedisClusterConnection.clusterGetNodes` or it can be constructed by using either the host and the port or the node Id. + +The following example shows a set of commands being run across the cluster: + +*Example 1. Sample of Running Commands Across the Cluster* + +```text +redis-cli@127.0.0.1:7379 > cluster nodes + +6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 // (1) +7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-20252 // (2) +164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-20253 // (3) +b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected // (4) +``` + +```java +RedisClusterConnection connection = connectionFactory.getClusterConnection(); + +connection.set("thing1", value); // (5) +connection.set("thing2", value); // (6) + +connection.keys("*"); // (7) + +connection.keys(NODE_7379, "*"); // (8) +connection.keys(NODE_7380, "*"); // (9) +connection.keys(NODE_7381, "*"); // (10) +connection.keys(NODE_7382, "*"); // (11) +``` +```text +1. Master node serving slots 0 to 5460 replicated to replica at 7382 +2. Master node serving slots 5461 to 10922 +3. Master node serving slots 10923 to 16383 +4. Replica node holding replicants of the master at 7379 +5. Request routed to node at 7381 serving slot 12182 +6. Request routed to node at 7379 serving slot 5061 +7. Request routed to nodes at 7379, 7380, 7381 → [thing1, thing2] +8. Request routed to node at 7379 → [thing2] +9. Request routed to node at 7380 → [] +10. Request routed to node at 7381 → [thing1] +11. Request routed to node at 7382 → [thing2] +``` + +When all keys map to the same slot, the native driver library automatically serves cross-slot requests, such as `MGET`. +However, once this is not the case, `RedisClusterConnection` runs multiple parallel `GET` commands against the slot-serving nodes and again returns an accumulated result. +This is less performant than the single-slot approach and, therefore, should be used with care. +If in doubt, consider pinning keys to the same slot by providing a prefix in curly brackets, such as `{my-prefix}.thing1` and `{my-prefix}.thing2`, which will both map to the same slot number. +The following example shows cross-slot request handling: + +*Example 2. Sample of Cross-Slot Request Handling* + +```text +redis-cli@127.0.0.1:7379 > cluster nodes + +6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 // (1) +7bb... +``` + +```java +RedisClusterConnection connection = connectionFactory.getClusterConnection(); + +connection.set("thing1", value); // slot: 12182 +connection.set("{thing1}.thing2", value); // slot: 12182 +connection.set("thing2", value); // slot: 5461 + +connection.mGet("thing1", "{thing1}.thing2"); // (2) + +connection.mGet("thing1", "thing2"); // (3) +``` +```text +1. Same Configuration as in the sample before. +2. Keys map to same slot → 127.0.0.1:7381 MGET thing1 {thing1}.thing2 +3. Keys map to different slots and get split up into single slot ones routed to the according nodes: + - 127.0.0.1:7379 GET thing2 + - 127.0.0.1:7381 GET thing1 +``` + +:::tip +The preceding examples demonstrate the general strategy followed by Spring Data Redis. +::: + +Be aware that some operations might require loading huge amounts of data into memory to compute the desired command. +Additionally, not all cross-slot requests can safely be ported to multiple single slot requests and error if misused (for example, `PFCOUNT`). + +## Working with `RedisTemplate` and `ClusterOperations` + +See the [Working with Objects through RedisTemplate](/redis/template) section for information about the general purpose, configuration, and usage of `RedisTemplate`. + +:::caution +Be careful when setting up `RedisTemplate#keySerializer` using any of the JSON `RedisSerializers`, as changing JSON structure has immediate influence on hash slot calculation. +::: + +`RedisTemplate` provides access to cluster-specific operations through the `ClusterOperations` interface, which can be obtained from `RedisTemplate.opsForCluster()`. +This lets you explicitly run commands on a single node within the cluster while retaining the serialization and deserialization features configured for the template. +It also provides administrative commands (such as `CLUSTER MEET`) or more high-level operations (for example, resharding). + +The following example shows how to access `RedisClusterConnection` with `RedisTemplate`: + +*Example 3. Accessing `RedisClusterConnection` with `RedisTemplate`* + +```java +ClusterOperations clusterOps = redisTemplate.opsForCluster(); +clusterOps.shutdown(NODE_7379); // (1) +``` +```text +1. Shut down node at 7379 and cross fingers there is a replica in place that can take over. +``` + +:::note +Redis Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +::: diff --git a/docs/src/content/docs/redis/connection-modes.md b/docs/src/content/docs/redis/connection-modes.md new file mode 100644 index 000000000..41a29bb90 --- /dev/null +++ b/docs/src/content/docs/redis/connection-modes.md @@ -0,0 +1,171 @@ +--- +title: Connection Modes +description: Connection Modes documentation +--- + +Redis can be operated in various setups. +Each mode of operation requires specific configuration that is explained in the following sections. + +## Redis Standalone + +The easiest way to get started is by using Redis Standalone with a single Redis server, + +Configure `org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory` or `org.springframework.data.redis.connection.jedis.JedisConnectionFactory`, as shown in the following example: + +```java +@Configuration +class RedisStandaloneConfiguration { + + /** + * Lettuce + */ + @Bean + public RedisConnectionFactory lettuceConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); + } + + /** + * Jedis + */ + @Bean + public RedisConnectionFactory jedisConnectionFactory() { + return new JedisConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); + } +} +``` + +## Write to Master, Read from Replica + +The Redis Master/Replica setup -- without automatic failover (for automatic failover see: [Sentinel](/redis/connection-modes#redis-sentinel)) -- not only allows data to be safely stored at more nodes. +It also allows, by using [Lettuce](/redis/drivers#configuring-the-lettuce-connector), reading data from replicas while pushing writes to the master. +You can set the read/write strategy to be used by using `LettuceClientConfiguration`, as shown in the following example: + +```java +@Configuration +class WriteToMasterReadFromReplicaConfiguration { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .readFrom(REPLICA_PREFERRED) + .build(); + + RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("server", 6379); + + return new LettuceConnectionFactory(serverConfig, clientConfig); + } +} +``` + +:::tip +For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration` instead of `org.springframework.data.redis.connection.RedisStandaloneConfiguration`. Please note that `RedisStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. +::: + +## Redis Sentinel + +For dealing with high-availability Redis, Spring Data Redis has support for [Redis Sentinel](https://redis.io/topics/sentinel), using `org.springframework.data.redis.connection.RedisSentinelConfiguration`, as shown in the following example: + +```java +/** + * Lettuce + */ +@Bean +public RedisConnectionFactory lettuceConnectionFactory() { + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() + .master("mymaster") + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + return new LettuceConnectionFactory(sentinelConfig); +} + +/** + * Jedis + */ +@Bean +public RedisConnectionFactory jedisConnectionFactory() { + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() + .master("mymaster") + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + return new JedisConnectionFactory(sentinelConfig); +} +``` + +:::tip +`RedisSentinelConfiguration` can also be defined through `RedisSentinelConfiguration.of(PropertySource)`, which lets you pick up the following properties: + +*Configuration Properties* + +* `spring.redis.sentinel.master`: name of the master node. +* `spring.redis.sentinel.nodes`: Comma delimited list of host:port pairs. +* `spring.redis.sentinel.username`: The username to apply when authenticating with Redis Sentinel (requires Redis 6) +* `spring.redis.sentinel.password`: The password to apply when authenticating with Redis Sentinel +* `spring.redis.sentinel.dataNode.username`: The username to apply when authenticating with Redis Data Node +* `spring.redis.sentinel.dataNode.password`: The password to apply when authenticating with Redis Data Node +* `spring.redis.sentinel.dataNode.database`: The database index to apply when authenticating with Redis Data Node +::: + +Sometimes, direct interaction with one of the Sentinels is required. Using `RedisConnectionFactory.getSentinelConnection()` or `RedisConnection.getSentinelCommands()` gives you access to the first active Sentinel configured. + +## Redis Cluster + +[Cluster support](/redis/cluster) is based on the same building blocks as non-clustered communication. `org.springframework.data.redis.connection.RedisClusterConnection`, an extension to `RedisConnection`, handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. +`RedisClusterConnection` instances are created with the `RedisConnectionFactory`, which has to be set up with the associated `org.springframework.data.redis.connection.RedisClusterConfiguration`, as shown in the following example: + +*Example 1. Sample RedisConnectionFactory Configuration for Redis Cluster* + +```java +@Component +@ConfigurationProperties(prefix = "spring.redis.cluster") +public class ClusterConfigurationProperties { + + /* + * spring.redis.cluster.nodes[0] = 127.0.0.1:7379 + * spring.redis.cluster.nodes[1] = 127.0.0.1:7380 + * ... + */ + List nodes; + + /** + * Get initial collection of known cluster nodes in format {@code host:port}. + * + * @return + */ + public List getNodes() { + return nodes; + } + + public void setNodes(List nodes) { + this.nodes = nodes; + } +} + +@Configuration +public class AppConfig { + + /** + * Type safe representation of application.properties + */ + @Autowired ClusterConfigurationProperties clusterProperties; + + public @Bean RedisConnectionFactory connectionFactory() { + + return new LettuceConnectionFactory( + new RedisClusterConfiguration(clusterProperties.getNodes())); + } +} +``` + +:::tip +`RedisClusterConfiguration` can also be defined through `RedisClusterConfiguration.of(PropertySource)`, which lets you pick up the following properties: + +*Configuration Properties* + +* `spring.redis.cluster.nodes`: Comma-delimited list of host:port pairs. +* `spring.redis.cluster.max-redirects`: Number of allowed cluster redirections. +::: + +:::note +The initial configuration points driver libraries to an initial set of cluster nodes. Changes resulting from live cluster reconfiguration are kept only in the native driver and are not written back to the configuration. +::: diff --git a/docs/src/content/docs/redis/drivers.md b/docs/src/content/docs/redis/drivers.md new file mode 100644 index 000000000..92c80ea1c --- /dev/null +++ b/docs/src/content/docs/redis/drivers.md @@ -0,0 +1,183 @@ +--- +title: Drivers +description: Drivers documentation +--- + +One of the first tasks when using Redis and Spring is to connect to the store through the IoC container. +To do that, a Java connector (or binding) is required. +No matter the library you choose, you need to use only one set of Spring Data Redis APIs (which behaves consistently across all connectors). +The `org.springframework.data.redis.connection` package and its `RedisConnection` and `RedisConnectionFactory` interfaces for working with and retrieving active connections to Redis. + +## RedisConnection and RedisConnectionFactory + +`RedisConnection` provides the core building block for Redis communication, as it handles the communication with the Redis backend. +It also automatically translates underlying connecting library exceptions to Spring's consistent [DAO exception hierarchy](https://docs.spring.io/spring-framework/reference/data-access.html#dao-exceptions) so that you can switch connectors without any code changes, as the operation semantics remain the same. + +:::note +For the corner cases where the native library API is required, `RedisConnection` provides a dedicated method (`getNativeConnection`) that returns the raw, underlying object used for communication. +::: + +Active `RedisConnection` objects are created through `RedisConnectionFactory`. +In addition, the factory acts as `PersistenceExceptionTranslator` objects, meaning that, once declared, they let you do transparent exception translation. +For example, you can do exception translation through the use of the `@Repository` annotation and AOP. +For more information, see the dedicated [section](https://docs.spring.io/spring-framework/reference/data-access.html#orm-exception-translation) in the Spring Framework documentation. + +:::note +`RedisConnection` classes are **not** Thread-safe. While the underlying native connection, such as Lettuce's `StatefulRedisConnection`, may be Thread-safe, Spring Data Redis's `LettuceConnection` class itself is not. Therefore, you should **not** share instances of a `RedisConnection` across multiple Threads. This is especially true for transactional, or blocking Redis operations and commands, such as `BLPOP`. In transactional and pipelining operations, for instance, `RedisConnection` holds onto unguarded mutable state to complete the operation correctly, thereby making it unsafe to use with multiple Threads. This is by design. +::: + +:::tip +If you need to share (stateful) Redis resources, like connections, across multiple Threads, for performance reasons or otherwise, then you should acquire the native connection and use the Redis client library (driver) API directly. Alternatively, you can use the `RedisTemplate`, which acquires and manages connections for operations (and Redis commands) in a Thread-safe manner. See [documentation](/redis/template) on `RedisTemplate` for more details. +::: + +:::note +Depending on the underlying configuration, the factory can return a new connection or an existing connection (when a pool or shared native connection is used). +::: + +The easiest way to work with a `RedisConnectionFactory` is to configure the appropriate connector through the IoC container and inject it into the using class. + +Unfortunately, currently, not all connectors support all Redis features. +When invoking a method on the Connection API that is unsupported by the underlying library, an `UnsupportedOperationException` is thrown. +The following overview explains features that are supported by the individual Redis connectors: + +*Table 1. Feature Availability across Redis Connectors* + +| Supported Feature | Lettuce | Jedis | +|-------------------|---------|-------| +| Standalone Connections | X | X | +| [Master/Replica Connections](/redis/connection-modes#write-to-master-read-from-replica) | X | X | +| [Redis Sentinel](/redis/connection-modes#redis-sentinel) | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | +| [Redis Cluster](/redis/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | +| Transport Channels | TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets | TCP | +| Connection Pooling | X (using `commons-pool2`) | X (using `commons-pool2`) | +| Other Connection Features | Singleton-connection sharing for non-blocking commands | Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. | +| SSL Support | X | X | +| [Pub/Sub](/redis/pubsub) | X | X | +| [Pipelining](/redis/pipelining) | X | X (Pipelining and Transactions mutually exclusive) | +| [Transactions](/redis/transactions) | X | X (Pipelining and Transactions mutually exclusive) | +| Datatype support | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | +| Reactive (non-blocking) API | X | | + +## Configuring the Lettuce Connector + +[Lettuce](https://github.com/lettuce-io/lettuce-core) is a [Netty](https://netty.io/)-based open-source connector supported by Spring Data Redis through the `org.springframework.data.redis.connection.lettuce` package. + +*Add the following to the pom.xml files `dependencies` element:* + +```xml + + + + + + io.lettuce + lettuce-core + 6.3.2.RELEASE + + + +``` + +The following example shows how to create a new Lettuce connection factory: + +```java +@Configuration +class AppConfig { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + + return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); + } +} +``` + +There are also a few Lettuce-specific connection parameters that can be tweaked. +By default, all `LettuceConnection` instances created by the `LettuceConnectionFactory` share the same thread-safe native connection for all non-blocking and non-transactional operations. +To use a dedicated connection each time, set `shareNativeConnection` to `false`. `LettuceConnectionFactory` can also be configured to use a `LettucePool` for pooling blocking and transactional connections or all connections if `shareNativeConnection` is set to `false`. + +The following example shows a more sophisticated configuration, including SSL and timeouts, that uses `LettuceClientConfigurationBuilder`: + +```java +@Bean +public LettuceConnectionFactory lettuceConnectionFactory() { + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .useSsl().and() + .commandTimeout(Duration.ofSeconds(2)) + .shutdownTimeout(Duration.ZERO) + .build(); + + return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379), clientConfig); +} +``` + +For more detailed client configuration tweaks, see `org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration`. + +Lettuce integrates with Netty's [native transports](https://netty.io/wiki/native-transports.html), letting you use Unix domain sockets to communicate with Redis. +Make sure to include the appropriate native transport dependencies that match your runtime environment. +The following example shows how to create a Lettuce Connection factory for a Unix domain socket at `/var/run/redis.sock`: + +```java +@Configuration +class AppConfig { + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + + return new LettuceConnectionFactory(new RedisSocketConfiguration("/var/run/redis.sock")); + } +} +``` + +:::note +Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for OS-native transport. +::: + +## Configuring the Jedis Connector + +[Jedis](https://github.com/redis/jedis) is a community-driven connector supported by the Spring Data Redis module through the `org.springframework.data.redis.connection.jedis` package. + +*Add the following to the pom.xml files `dependencies` element:* + +```xml + + + + + + redis.clients + jedis + 5.1.2 + + + +``` + +In its simplest form, the Jedis configuration looks as follow: + +```java +@Configuration +class AppConfig { + + @Bean + public JedisConnectionFactory redisConnectionFactory() { + return new JedisConnectionFactory(); + } +} +``` + +For production use, however, you might want to tweak settings such as the host or password, as shown in the following example: + +```java +@Configuration +class RedisConfiguration { + + @Bean + public JedisConnectionFactory redisConnectionFactory() { + + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("server", 6379); + return new JedisConnectionFactory(config); + } +} +``` diff --git a/docs/src/content/docs/redis/getting-started.mdx b/docs/src/content/docs/redis/getting-started.mdx new file mode 100644 index 000000000..a4d2aefbd --- /dev/null +++ b/docs/src/content/docs/redis/getting-started.mdx @@ -0,0 +1,86 @@ +--- +title: Getting Started +description: Getting Started documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-redis) or create a Spring project in [Spring Tools](https://spring.io/tools). +## Examples Repository + +The GitHub [spring-data-examples repository](https://github.com/spring-projects/spring-data-examples) hosts several examples that you can download and play around with to get a feel for how the library works. +## Hello World + +First, you need to set up a running Redis server. +Spring Data Redis requires Redis 2.6 or above and Spring Data Redis integrates with [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/redis/jedis), two popular open-source Java libraries for Redis. + +Now you can create a simple Java application that stores and reads a value to and from Redis. + +Create the main application to run, as the following example shows: + + + + +```java +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +public class RedisApplication { + + private static final Log LOG = LogFactory.getLog(RedisApplication.class); + + public static void main(String[] args) { + + RedisConnectionFactory factory = new LettuceConnectionFactory(); + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + template.afterPropertiesSet(); + + template.opsForValue().set("foo", "bar"); + + LOG.info("Value at foo:" + template.opsForValue().get("foo")); + + template.getConnectionFactory().getConnection().close(); + } +} +``` + + + + +```java +import reactor.core.publisher.Mono; + +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.serialization.RedisSerializationContext; + +public class ReactiveRedisApplication { + + public static void main(String[] args) { + + ReactiveRedisConnectionFactory factory = new LettuceConnectionFactory(); + + ReactiveRedisTemplate template = new ReactiveRedisTemplate<>(factory, RedisSerializationContext.string()); + + template.opsForValue().set("foo", "bar") + .then(template.opsForValue().get("foo")) + .doOnNext(System.out::println) + .then(Mono.fromRunnable(() -> factory.getReactiveConnection().close())) + .subscribe(); + } +} +``` + + + +Even in this simple example, there are a few notable things to point out: + +* You can create an instance of `org.springframework.data.redis.core.RedisTemplate` (or `org.springframework.data.redis.core.ReactiveRedisTemplate`for reactive usage) with a `org.springframework.data.redis.connection.RedisConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. +* There's no single way to use Redis as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/redis/hash-mappers.md b/docs/src/content/docs/redis/hash-mappers.md new file mode 100644 index 000000000..d5030c2b7 --- /dev/null +++ b/docs/src/content/docs/redis/hash-mappers.md @@ -0,0 +1,109 @@ +--- +title: Hash Mappers +description: Hash Mappers documentation +--- + +Data can be stored by using various data structures within Redis. `org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer` can convert objects in [JSON](https://en.wikipedia.org/wiki/JSON) format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): + +* Direct mapping, by using `org.springframework.data.redis.core.HashOperations` and a [serializer](/redis/template#serializers) +* Using [Redis Repositories](/repositories) +* Using `org.springframework.data.redis.hash.HashMapper` and `org.springframework.data.redis.core.HashOperations` + +## Hash Mappers + +Hash mappers are converters of map objects to a `Map` and back. `org.springframework.data.redis.hash.HashMapper` is intended for using with Redis Hashes. + +Multiple implementations are available: + +* `org.springframework.data.redis.hash.BeanUtilsHashMapper` using Spring's [BeanUtils](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html). +* `org.springframework.data.redis.hash.ObjectHashMapper` using [Object-to-Hash Mapping](/redis/redis-repositories/mapping). +* [`Jackson2HashMapper`](#jackson2hashmapper) using [FasterXML Jackson](https://github.com/FasterXML/jackson). + +The following example shows one way to implement hash mapping: + +```java +public class Person { + String firstname; + String lastname; + + // … +} + +public class HashMapping { + + @Resource(name = "redisTemplate") + HashOperations hashOperations; + + HashMapper mapper = new ObjectHashMapper(); + + public void writeHash(String key, Person person) { + + Map mappedHash = mapper.toHash(person); + hashOperations.putAll(key, mappedHash); + } + + public Person loadHash(String key) { + + Map loadedHash = hashOperations.entries(key); + return (Person) mapper.fromHash(loadedHash); + } +} +``` + +## Jackson2HashMapper + +`org.springframework.data.redis.hash.Jackson2HashMapper` provides Redis Hash mapping for domain objects by using [FasterXML Jackson](https://github.com/FasterXML/jackson). +`Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. +Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. + +Flattening creates individual hash entries for all nested properties and resolves complex types into simple types, as far as possible. + +Consider the following class and the data structure it contains: + +```java +public class Person { + String firstname; + String lastname; + Address address; + Date date; + LocalDateTime localDateTime; +} + +public class Address { + String city; + String country; +} +``` + +The following table shows how the data in the preceding class would appear in normal mapping: + +*Table 1. Normal Mapping* + +| Hash Field | Value | +|------------|-------| +| firstname | `Jon` | +| lastname | `Snow` | +| address | `{ "city" : "Castle Black", "country" : "The North" }` | +| date | `1561543964015` | +| localDateTime | `2018-01-02T12:13:14` | + +The following table shows how the data in the preceding class would appear in flat mapping: + +*Table 2. Flat Mapping* + +| Hash Field | Value | +|------------|-------| +| firstname | `Jon` | +| lastname | `Snow` | +| address.city | `Castle Black` | +| address.country | `The North` | +| date | `1561543964015` | +| localDateTime | `2018-01-02T12:13:14` | + +:::note +Flattening requires all property names to not interfere with the JSON path. Using dots or brackets in map keys or as property names is not supported when you use flattening. The resulting hash cannot be mapped back into an Object. +::: + +:::note +`java.util.Date` and `java.util.Calendar` are represented with milliseconds. JSR-310 Date/Time types are serialized to their `toString` form if `jackson-datatype-jsr310` is on the class path. +::: diff --git a/docs/src/content/docs/redis/pipelining.md b/docs/src/content/docs/redis/pipelining.md new file mode 100644 index 000000000..938b3887e --- /dev/null +++ b/docs/src/content/docs/redis/pipelining.md @@ -0,0 +1,47 @@ +--- +title: Pipelining +description: Pipelining documentation +--- + +Redis provides support for [pipelining](https://redis.io/topics/pipelining), which involves sending multiple commands to the server without waiting for the replies and then reading the replies in a single step. Pipelining can improve performance when you need to send several commands in a row, such as adding many elements to the same List. + +Spring Data Redis provides several `RedisTemplate` methods for running commands in a pipeline. If you do not care about the results of the pipelined operations, you can use the standard `execute` method, passing `true` for the `pipeline` argument. The `executePipelined` methods run the provided `RedisCallback` or `SessionCallback` in a pipeline and return the results, as shown in the following example: + +```java +//pop a specified number of items from a queue +List results = stringRedisTemplate.executePipelined( + new RedisCallback() { + public Object doInRedis(RedisConnection connection) throws DataAccessException { + StringRedisConnection stringRedisConn = (StringRedisConnection)connection; + for(int i=0; i< batchSize; i++) { + stringRedisConn.rPop("myqueue"); + } + return null; + } +}); +``` + +The preceding example runs a bulk right pop of items from a queue in a pipeline. +The `results` `List` contains all the popped items. `RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results before returning, so the returned items in the preceding example are Strings. +There are additional `executePipelined` methods that let you pass a custom serializer for pipelined results. + +Note that the value returned from the `RedisCallback` is required to be `null`, as this value is discarded in favor of returning the results of the pipelined commands. + +:::tip +The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. +::: + +```java +LettuceConnectionFactory factory = // ... +factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3)); // (1) +``` +```text +1. Buffer locally and flush after every 3rd command. +``` + +:::note +Pipelining is limited to Redis Standalone. +::: + +Redis Cluster is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Same-slot keys are fully supported. diff --git a/docs/src/content/docs/redis/pubsub.mdx b/docs/src/content/docs/redis/pubsub.mdx new file mode 100644 index 000000000..2b64f166b --- /dev/null +++ b/docs/src/content/docs/redis/pubsub.mdx @@ -0,0 +1,231 @@ +--- +title: Pubsub +description: Pubsub documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Spring Data provides dedicated messaging integration for Redis, similar in functionality and naming to the JMS integration in Spring Framework. + +Redis messaging can be roughly divided into two areas of functionality: + +* Publication or production of messages +* Subscription or consumption of messages + +This is an example of the pattern often called Publish/Subscribe (Pub/Sub for short). The `RedisTemplate` class is used for message production. For asynchronous reception similar to Java EE's message-driven bean style, Spring Data provides a dedicated message listener container that is used to create Message-Driven POJOs (MDPs) and, for synchronous reception, the `RedisConnection` contract. + +The `org.springframework.data.redis.connection` and `org.springframework.data.redis.listener` packages provide the core functionality for Redis messaging. + +## Publishing (Sending Messages) + +To publish a message, you can use, as with the other operations, either the low-level `[Reactive]RedisConnection` or the high-level `[Reactive]RedisOperations`. Both entities offer the `publish` method, which accepts the message and the destination channel as arguments. While `RedisConnection` requires raw data (array of bytes), the `[Reactive]RedisOperations` lets arbitrary objects be passed in as messages, as shown in the following example: + + + + +```java +// send message through connection +RedisConnection con = … +byte[] msg = … +byte[] channel = … +con.pubSubCommands().publish(msg, channel); + +// send message through RedisOperations +RedisOperations operations = … +Long numberOfClients = operations.convertAndSend("hello!", "world"); +``` + + + + +```java +// send message through connection +ReactiveRedisConnection con = … +ByteBuffer[] msg = … +ByteBuffer[] channel = … +con.pubSubCommands().publish(msg, channel); + +// send message through ReactiveRedisOperations +ReactiveRedisOperations operations = … +Mono numberOfClients = operations.convertAndSend("hello!", "world"); +``` + + + + +## Subscribing (Receiving Messages) + +On the receiving side, one can subscribe to one or multiple channels either by naming them directly or by using pattern matching. The latter approach is quite useful, as it not only lets multiple subscriptions be created with one command but can also listen on channels not yet created at subscription time (as long as they match the pattern). + +At the low-level, `RedisConnection` offers the `subscribe` and `pSubscribe` methods that map the Redis commands for subscribing by channel or by pattern, respectively. Note that multiple channels or patterns can be used as arguments. To change the subscription of a connection or query whether it is listening, `RedisConnection` provides the `getSubscription` and `isSubscribed` methods. + +:::note +Subscription commands in Spring Data Redis are blocking. That is, calling subscribe on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the subscription is canceled, which happens when another thread invokes `unsubscribe` or `pUnsubscribe` on the *same* connection. See "[Message Listener Containers](#message-listener-containers)" (later in this document) for a solution to this problem. +::: + +As mentioned earlier, once subscribed, a connection starts waiting for messages. Only commands that add new subscriptions, modify existing subscriptions, and cancel existing subscriptions are allowed. Invoking anything other than `subscribe`, `pSubscribe`, `unsubscribe`, or `pUnsubscribe` throws an exception. + +In order to subscribe to messages, one needs to implement the `MessageListener` callback. Each time a new message arrives, the callback gets invoked and the user code gets run by the `onMessage` method. The interface gives access not only to the actual message but also to the channel it has been received through and the pattern (if any) used by the subscription to match the channel. This information lets the callee differentiate between various messages not just by content but also examining additional details. + +### Message Listener Containers + +Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `org.springframework.data.redis.listener.RedisMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). + +`org.springframework.data.redis.listener.RedisMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Redis channel and drive the `org.springframework.data.redis.connection.MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. + +A `org.springframework.data.redis.connection.MessageListener` can additionally implement `org.springframework.data.redis.connection.SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. + +Furthermore, to minimize the application footprint, `org.springframework.data.redis.listener.RedisMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. + +To help with the asynchronous nature of messages, the container requires a `java.util.concurrent.Executor` (or Spring's `TaskExecutor`) for dispatching the messages. Depending on the load, the number of listeners, or the runtime environment, you should change or tweak the executor to better serve your needs. In particular, in managed environments (such as app servers), it is highly recommended to pick a proper `TaskExecutor` to take advantage of its runtime. + +### The MessageListenerAdapter + +The `org.springframework.data.redis.listener.adapter.MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). + +Consider the following interface definition: + +```java +public interface MessageDelegate { + void handleMessage(String message); + void handleMessage(Map message); + void handleMessage(byte[] message); + void handleMessage(Serializable message); + // pass the channel/pattern as well + void handleMessage(Serializable message, String channel); + } +``` + +Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `org.springframework.data.redis.listener.adapter.MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: + +```java +public class DefaultMessageDelegate implements MessageDelegate { + // implementation elided for clarity... +} +``` + +Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Redis dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: + + + + +```java +@Configuration +class MyConfig { + + // … + + @Bean + DefaultMessageDelegate listener() { + return new DefaultMessageDelegate(); + } + + @Bean + MessageListenerAdapter messageListenerAdapter(DefaultMessageDelegate listener) { + return new MessageListenerAdapter(listener, "handleMessage"); + } + + @Bean + RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(listener, ChannelTopic.of("chatroom")); + return container; + } +} +``` + + + + +```xml + + + + + + + + + + + ... + +``` + + + + +:::note +The listener topic can be either a channel (for example, `topic="chatroom"` respective `Topic.channel("chatroom")`) or a pattern (for example, `topic="*room"` respective `Topic.pattern("*room")`). +::: + +The preceding example uses the Redis namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: + +```xml + + + + + + + + + + + + + + + + + + +``` + +Each time a message is received, the adapter automatically and transparently performs translation (using the configured `RedisSerializer`) between the low-level format and the required object type. Any exception caused by the method invocation is caught and handled by the container (by default, exceptions get logged). + +## Reactive Message Listener Container + +Spring Data offers `org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. + +The message listener container itself does not require external threading resources. It uses the driver threads to publish messages. + +```java +ReactiveRedisConnectionFactory factory = … +ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); + +Flux> stream = container.receive(ChannelTopic.of("my-channel")); +``` + +To await and ensure proper subscription, you can use the `receiveLater` method that returns a `Mono>`. +The resulting `Mono` completes with an inner publisher as a result of completing the subscription to the given topics. By intercepting `onNext` signals, you can synchronize server-side subscriptions. + +```java +ReactiveRedisConnectionFactory factory = … +ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); + +Mono>> stream = container.receiveLater(ChannelTopic.of("my-channel")); + +stream.doOnNext(inner -> // notification hook when Redis subscriptions are synchronized with the server) + .flatMapMany(Function.identity()) + .…; +``` + +### Subscribing via template API + +As mentioned above you can directly use `org.springframework.data.redis.core.ReactiveRedisTemplate` to subscribe to channels / patterns. This approach +offers a straight forward, though limited solution as you lose the option to add subscriptions after the initial +ones. Nevertheless you still can control the message stream via the returned `Flux` using eg. `take(Duration)`. When +done reading, on error or cancellation all bound resources are freed again. + +```java +redisTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> { + // message processing ... +}).subscribe(); +``` diff --git a/docs/src/content/docs/redis/redis-cache.md b/docs/src/content/docs/redis/redis-cache.md new file mode 100644 index 000000000..1e9aea5e3 --- /dev/null +++ b/docs/src/content/docs/redis/redis-cache.md @@ -0,0 +1,269 @@ +--- +title: Redis Cache +description: Redis Cache documentation +--- + +Spring Data Redis provides an implementation of Spring Framework's [Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration.html#cache) in the `org.springframework.data.redis.cache` package. +To use Redis as a backing implementation, add `org.springframework.data.redis.cache.RedisCacheManager` to your configuration, as follows: + +```java +@Bean +public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + return RedisCacheManager.create(connectionFactory); +} +``` + +`RedisCacheManager` behavior can be configured with `org.springframework.data.redis.cache.RedisCacheManager$RedisCacheManagerBuilder`, letting you set the default `org.springframework.data.redis.cache.RedisCacheManager`, transaction behavior, and predefined caches. + +```java +RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) + .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) + .transactionAware() + .withInitialCacheConfigurations(Collections.singletonMap("predefined", + RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues())) + .build(); +``` + +As shown in the preceding example, `RedisCacheManager` allows custom configuration on a per-cache basis. + +The behavior of `org.springframework.data.redis.cache.RedisCache` created by `org.springframework.data.redis.cache.RedisCacheManager` is defined with `RedisCacheConfiguration`. +The configuration lets you set key expiration times, prefixes, and `RedisSerializer` implementations for converting to and from the binary storage format, as shown in the following example: + +```java +RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(1)) + .disableCachingNullValues(); +``` + +`org.springframework.data.redis.cache.RedisCacheManager` defaults to a lock-free `org.springframework.data.redis.cache.RedisCacheWriter` for reading and writing binary values. +Lock-free caching improves throughput. +The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Redis. +The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. + +Locking applies on the *cache level*, not per *cache entry*. + +It is possible to opt in to the locking behavior as follows: + +```java +RedisCacheManager cacheManager = RedisCacheManager + .builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) + .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) + ... +``` + +By default, any `key` for a cache entry gets prefixed with the actual cache name followed by two colons. +This behavior can be changed to a static as well as a computed prefix. + +The following example shows how to set a static prefix: + +```java +// static key prefix +RedisCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)"); +``` + +The following example shows how to set a computed prefix: + +```java +// computed key prefix +RedisCacheConfiguration.defaultCacheConfig() + .computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName); +``` + +The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `KEYS` can cause performance issues with large keyspaces. +Therefore, the default `RedisCacheWriter` can be created with a `BatchStrategy` to switch to a `SCAN`-based batch strategy. +The `SCAN` strategy requires a batch size to avoid excessive Redis command round trips: + +```java +RedisCacheManager cacheManager = RedisCacheManager + .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) + .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) + ... +``` + +:::note +The `KEYS` batch strategy is fully supported using any driver and Redis operation mode (Standalone, Clustered). +::: + +`SCAN` is fully supported when using the Lettuce driver. +Jedis supports `SCAN` only in non-clustered modes. + +The following table lists the default settings for `RedisCacheManager`: + +*Table 1. `RedisCacheManager` defaults* + +| Setting | Value | +|---------|-------| +| Cache Writer | Non-locking, `KEYS` batch strategy | +| Cache Configuration | `RedisCacheConfiguration#defaultConfiguration` | +| Initial Caches | None | +| Transaction Aware | No | + +The following table lists the default settings for `RedisCacheConfiguration`: + +*Table 2. RedisCacheConfiguration defaults* + +| Setting | Value | +|---------|-------| +| Key Expiration | None | +| Cache `null` | Yes | +| Prefix Keys | Yes | +| Default Prefix | The actual cache name | +| Key Serializer | `StringRedisSerializer` | +| Value Serializer | `JdkSerializationRedisSerializer` | +| Conversion Service | `DefaultFormattingConversionService` with default cache key converters | + +:::note +By default `RedisCache`, statistics are disabled. +::: + +Use `RedisCacheManagerBuilder.enableStatistics()` to collect local _hits_ and _misses_ through `RedisCache#getStatistics()`, returning a snapshot of the collected data. + +## Redis Cache Expiration + +The implementation of time-to-idle (TTI) as well as time-to-live (TTL) varies in definition and behavior even across different data stores. + +In general: + +* _time-to-live_ (TTL) _expiration_ - TTL is only set and reset by a create or update data access operation. +As long as the entry is written before the TTL expiration timeout, including on creation, an entry's timeout will reset to the configured duration of the TTL expiration timeout. +For example, if the TTL expiration timeout is set to 5 minutes, then the timeout will be set to 5 minutes on entry creation and reset to 5 minutes anytime the entry is updated thereafter and before the 5-minute interval expires. +If no update occurs within 5 minutes, even if the entry was read several times, or even just read once during the 5-minute interval, the entry will still expire. +The entry must be written to prevent the entry from expiring when declaring a TTL expiration policy. + +* _time-to-idle_ (TTI) _expiration_ - TTI is reset anytime the entry is also read as well as for entry updates, and is effectively and extension to the TTL expiration policy. + +:::note +Some data stores expire an entry when TTL is configured no matter what type of data access operation occurs on the entry (reads, writes, or otherwise). +After the set, configured TTL expiration timeout, the entry is evicted from the data store regardless. +Eviction actions (for example: destroy, invalidate, overflow-to-disk (for persistent stores), etc.) are data store specific. +::: + +### Time-To-Live (TTL) Expiration + +Spring Data Redis's `Cache` implementation supports _time-to-live_ (TTL) expiration on cache entries. +Users can either configure the TTL expiration timeout with a fixed `Duration` or a dynamically computed `Duration` per cache entry by supplying an implementation of the new `RedisCacheWriter.TtlFunction` interface. + +:::tip +The `RedisCacheWriter.TtlFunction` interface was introduced in Spring Data Redis `3.2.0`. +::: + +If all cache entries should expire after a set duration of time, then simply configure a TTL expiration timeout with a fixed `Duration`, as follows: + +```java +RedisCacheConfiguration fiveMinuteTtlExpirationDefaults = + RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)); +``` + +However, if the TTL expiration timeout should vary by cache entry, then you must provide a custom implementation of the `RedisCacheWriter.TtlFunction` interface: + +```java +enum MyCustomTtlFunction implements TtlFunction { + + INSTANCE; + + @Override + public Duration getTimeToLive(Object key, @Nullable Object value) { + // compute a TTL expiration timeout (Duration) based on the cache entry key and/or value + } +} +``` + +:::note +Under-the-hood, a fixed `Duration` TTL expiration is wrapped in a `TtlFunction` implementation returning the provided `Duration`. +::: + +Then, you can either configure the fixed `Duration` or the dynamic, per-cache entry `Duration` TTL expiration on a global basis using: + +*Global fixed Duration TTL expiration timeout* + +```java +RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(fiveMinuteTtlExpirationDefaults) + .build(); +``` + +Or, alternatively: + +*Global, dynamically computed per-cache entry Duration TTL expiration timeout* + +```java +RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(MyCustomTtlFunction.INSTANCE); + +RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(defaults) + .build(); +``` + +Of course, you can combine both global and per-cache configuration using: + +*Global fixed Duration TTL expiration timeout* + +```java +RedisCacheConfiguration predefined = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(MyCustomTtlFunction.INSTANCE); + +Map initialCaches = Collections.singletonMap("predefined", predefined); + +RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(fiveMinuteTtlExpirationDefaults) + .withInitialCacheConfigurations(initialCaches) + .build(); +``` + +### Time-To-Idle (TTI) Expiration + +Redis itself does not support the concept of true, time-to-idle (TTI) expiration. +Still, using Spring Data Redis's Cache implementation, it is possible to achieve time-to-idle (TTI) expiration-like behavior. + +The configuration of TTI in Spring Data Redis's Cache implementation must be explicitly enabled, that is, is opt-in. +Additionally, you must also provide TTL configuration using either a fixed `Duration` or a custom implementation of the `TtlFunction` interface as described above in [Redis Cache Expiration](#redis-cache-expiration). + +For example: + +```java +@Configuration +@EnableCaching +class RedisConfiguration { + + @Bean + RedisConnectionFactory redisConnectionFactory() { + // ... + } + + @Bean + RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + + RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(5)) + .enableTimeToIdle(); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(defaults) + .build(); + } +} +``` + +Because Redis servers do not implement a proper notion of TTI, then TTI can only be achieved with Redis commands accepting expiration options. +In Redis, the "expiration" is technically a time-to-live (TTL) policy. +However, TTL expiration can be passed when reading the value of a key thereby effectively resetting the TTL expiration timeout, as is now the case in Spring Data Redis's `Cache.get(key)` operation. + +`RedisCache.get(key)` is implemented by calling the Redis `GETEX` command. + +:::danger +The Redis [`GETEX`](https://redis.io/commands/getex) command is only available in Redis version `6.2.0` and later. +Therefore, if you are not using Redis `6.2.0` or later, then it is not possible to use Spring Data Redis's TTI expiration. +A command execution exception will be thrown if you enable TTI against an incompatible Redis (server) version. +No attempt is made to determine if the Redis server version is correct and supports the `GETEX` command. +::: + +:::danger +In order to achieve true time-to-idle (TTI) expiration-like behavior in your Spring Data Redis application, then an entry must be consistently accessed with (TTL) expiration on every read or write operation. +There are no exceptions to this rule. +If you are mixing and matching different data access patterns across your Spring Data Redis application (for example: caching, invoking operations using `RedisTemplate` and possibly, or especially when using Spring Data Repository CRUD operations), then accessing an entry may not necessarily prevent the entry from expiring if TTL expiration was set. +For example, an entry maybe "put" in (written to) the cache during a `@Cacheable` service method invocation with a TTL expiration (i.e. `SET `) and later read using a Spring Data Redis Repository before the expiration timeout (using `GET` without expiration options). +A simple `GET` without specifying expiration options will not reset the TTL expiration timeout on an entry. +Therefore, the entry may expire before the next data access operation, even though it was just read. +Since this cannot be enforced in the Redis server, then it is the responsibility of your application to consistently access an entry when time-to-idle expiration is configured, in and outside of caching, where appropriate. +::: diff --git a/docs/src/content/docs/redis/redis-repositories/anatomy.md b/docs/src/content/docs/redis/redis-repositories/anatomy.md new file mode 100644 index 000000000..9aa1f0b22 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/anatomy.md @@ -0,0 +1,122 @@ +--- +title: Redis Repositories Anatomy +description: Anatomy documentation +--- + +Redis as a store itself offers a very narrow low-level API leaving higher level functions, such as secondary indexes and query operations, up to the user. + +This section provides a more detailed view of commands issued by the repository abstraction for a better understanding of potential performance implications. + +Consider the following entity class as the starting point for all operations: + +*Example 1. Example entity* + +```java +@RedisHash("people") +public class Person { + + @Id String id; + @Indexed String firstname; + String lastname; + Address hometown; +} + +public class Address { + + @GeoIndexed Point location; +} +``` + +## Insert new + +```java +repository.save(new Person("rand", "al'thor")); +``` + +```text +HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9" "firstname" "rand" "lastname" "al'thor" // (1) +SADD "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9" // (2) +SADD "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9" // (3) +SADD "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand" // (4) +``` +```text +1. Save the flattened entry as hash. +2. Add the key of the hash written in (1) to the helper index of entities in the same keyspace. +3. Add the key of the hash written in (2) to the secondary index of firstnames with the properties value. +4. Add the index of (3) to the set of helper structures for entry to keep track of indexes to clean on delete/update. +``` + +## Replace existing + +```java +repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor")); +``` + +```text +DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" // (1) +HMSET "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor" // (2) +SADD "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" // (3) +SMEMBERS "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" // (4) +TYPE "people:firstname:rand" // (5) +SREM "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" // (6) +DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" // (7) +SADD "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" // (8) +SADD "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn" // (9) +``` +```text +1. Remove the existing hash to avoid leftovers of hash keys potentially no longer present. +2. Save the flattened entry as hash. +3. Add the key of the hash written in (1) to the helper index of entities in the same keyspace. +4. Get existing index structures that might need to be updated. +5. Check if the index exists and what type it is (text, geo, …). +6. Remove a potentially existing key from the index. +7. Remove the helper holding index information. +8. Add the key of the hash added in (2) to the secondary index of firstnames with the properties value. +9. Add the index of (6) to the set of helper structures for entry to keep track of indexes to clean on delete/update. +``` + +## Save Geo Data + +Geo indexes follow the same rules as normal text based ones but use geo structure to store values. +Saving an entity that uses a Geo-indexed property results in the following commands: + +```text +GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b" // (1) +SADD "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location" // (2) +``` +```text +1. Add the key of the saved entry to the the geo index. +2. Keep track of the index structure. +``` + +## Find using simple index + +```java +repository.findByFirstname("egwene"); +``` + +```text +SINTER "people:firstname:egwene" // (1) +HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3" // (2) +HGETALL ... +``` +```text +1. Fetch keys contained in the secondary index. +2. Fetch each key returned by (1) individually. +``` + +## Find using Geo Index + +```java +repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS)); +``` + +```text +GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" // (1) +HGETALL "people:76900e94-b057-44bc-abcf-8126d51a621b" // (2) +HGETALL ... +``` +```text +1. Fetch keys contained in the secondary index. +2. Fetch each key returned by (1) individually. +``` diff --git a/docs/src/content/docs/redis/redis-repositories/cdi-integration.md b/docs/src/content/docs/redis/redis-repositories/cdi-integration.md new file mode 100644 index 000000000..95e5252d3 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/cdi-integration.md @@ -0,0 +1,65 @@ +--- +title: CDI Integration +description: CDI Integration documentation +--- + +Instances of the repository interfaces are usually created by a container, for which Spring is the most natural choice when working with Spring Data. +Spring offers sophisticated support for creating bean instances. +Spring Data Redis ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. +The extension is part of the JAR, so, to activate it, drop the Spring Data Redis JAR into your classpath. + +You can then set up the infrastructure by implementing a CDI Producer for the `org.springframework.data.redis.connection.RedisConnectionFactory` and `org.springframework.data.redis.core.RedisOperations`, as shown in the following example: + +```java +class RedisOperationsProducer { + @Produces + RedisConnectionFactory redisConnectionFactory() { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration()); + connectionFactory.afterPropertiesSet(); + connectionFactory.start(); + + return connectionFactory; + } + + void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception { + + if (redisConnectionFactory instanceof DisposableBean) { + ((DisposableBean) redisConnectionFactory).destroy(); + } + } + + @Produces + @ApplicationScoped + RedisOperations redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) { + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + template.afterPropertiesSet(); + + return template; + } + +} +``` + +The necessary setup can vary, depending on your JavaEE environment. + +The Spring Data Redis CDI extension picks up all available repositories as CDI beans and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. +Thus, obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property, as shown in the following example: + +```java +class RepositoryClient { + + @Inject + PersonRepository repository; + + public void businessMethod() { + List people = repository.findAll(); + } +} +``` + +A Redis Repository requires `org.springframework.data.redis.core.RedisKeyValueAdapter` and `org.springframework.data.redis.core.RedisKeyValueTemplate` instances. +These beans are created and managed by the Spring Data CDI extension if no provided beans are found. +You can, however, supply your own beans to configure the specific properties of `org.springframework.data.redis.core.RedisKeyValueAdapter` and `org.springframework.data.redis.core.RedisKeyValueTemplate`. diff --git a/docs/src/content/docs/redis/redis-repositories/cluster.md b/docs/src/content/docs/redis/redis-repositories/cluster.md new file mode 100644 index 000000000..56f0e66d3 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/cluster.md @@ -0,0 +1,35 @@ +--- +title: Redis Repositories Running on a Cluster +description: Cluster documentation +--- + +You can use the Redis repository support in a clustered Redis environment. +See the "[Redis Cluster](/redis/cluster)" section for `ConnectionFactory` configuration details. +Still, some additional configuration must be done, because the default key distribution spreads entities and secondary indexes through out the whole cluster and its slots. + +The following table shows the details of data on a cluster (based on previous examples): + +## Default Key Distribution + +| Key | Type | Slot | Node | +|-----|------|------|------| +| people:e2c7dcee-b8cd-4424-883e-736ce564363e | id for hash | 15171 | 127.0.0.1:7381 | +| people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 | id for hash | 7373 | 127.0.0.1:7380 | +| people:firstname:rand | index | 1700 | 127.0.0.1:7379 | + +Some commands (such as `SINTER` and `SUNION`) can only be processed on the server side when all involved keys map to the same slot. +Otherwise, computation has to be done on client side. +Therefore, it is useful to pin keyspaces to a single slot, which lets make use of Redis server side computation right away. +The following table shows what happens when you do (note the change in the slot column and the port value in the node column): + +## Pinned Keyspace Distribution + +| Key | Type | Slot | Node | +|-----|------|------|------| +| {people}:e2c7dcee-b8cd-4424-883e-736ce564363e | id for hash | 2399 | 127.0.0.1:7379 | +| {people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 | id for hash | 2399 | 127.0.0.1:7379 | +| {people}:firstname:rand | index | 2399 | 127.0.0.1:7379 | + +:::tip +Define and pin keyspaces by using `@RedisHash("{yourkeyspace}")` to specific slots when you use Redis cluster. +::: diff --git a/docs/src/content/docs/redis/redis-repositories/expirations.md b/docs/src/content/docs/redis/redis-repositories/expirations.md new file mode 100644 index 000000000..9255d41d3 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/expirations.md @@ -0,0 +1,76 @@ +--- +title: Time To Live +description: Expirations documentation +--- + +Objects stored in Redis may be valid only for a certain amount of time. +This is especially useful for persisting short-lived objects in Redis without having to remove them manually when they reach their end of life. +The expiration time in seconds can be set with `@RedisHash(timeToLive=...)` as well as by using `org.springframework.data.redis.core.convert.KeyspaceConfiguration$KeyspaceSettings` (see [Keyspaces](/redis/redis-repositories/keyspaces)). + +More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or a method. +However, do not apply `@TimeToLive` on both a method and a property within the same class. +The following example shows the `@TimeToLive` annotation on a property and on a method: + +*Example 1. Expirations* + +```java +public class TimeToLiveOnProperty { + + @Id + private String id; + + @TimeToLive + private Long expiration; +} + +public class TimeToLiveOnMethod { + + @Id + private String id; + + @TimeToLive + public long getTimeToLive() { + return new Random().nextLong(); + } +} +``` + +:::note +Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no associated expiration. +::: + +The repository implementation ensures subscription to [Redis keyspace notifications](https://redis.io/topics/notifications) via `org.springframework.data.redis.listener.RedisMessageListenerContainer`. + +When the expiration is set to a positive value, the corresponding `EXPIRE` command is run. +In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. +This is done to enable the Repository support to publish `org.springframework.data.redis.core.RedisKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. +Expiry events are received on all connected applications that use Spring Data Redis repositories. + +By default, the key expiry listener is disabled when initializing the application. +The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. +See `org.springframework.data.redis.core.RedisKeyValueAdapter$EnableKeyspaceEvents` for possible values. + +The `RedisKeyExpiredEvent` holds a copy of the expired domain object as well as the key. + +:::note +Delaying or disabling the expiry event listener startup impacts `RedisKeyExpiredEvent` publishing. +A disabled event listener does not publish expiry events. +A delayed startup can cause loss of events because of the delayed listener initialization. +::: + +:::note +The keyspace notification message listener alters `notify-keyspace-events` settings in Redis, if those are not already set. +Existing settings are not overridden, so you must set up those settings correctly (or leave them empty). +Note that `CONFIG` is disabled on AWS ElastiCache, and enabling the listener leads to an error. +To work around this behavior, set the `keyspaceNotificationsConfigParameter` parameter to an empty string. +This prevents `CONFIG` command usage. +::: + +:::note +Redis Pub/Sub messages are not persistent. +If a key expires while the application is down, the expiry event is not processed, which may lead to secondary indexes containing references to the expired object. +::: + +:::note +`@EnableKeyspaceEvents(shadowCopy = OFF)` disable storage of phantom copies and reduces data size within Redis. `RedisKeyExpiredEvent` will only contain the `id` of the expired key. +::: diff --git a/docs/src/content/docs/redis/redis-repositories/indexes.md b/docs/src/content/docs/redis/redis-repositories/indexes.md new file mode 100644 index 000000000..170d31672 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/indexes.md @@ -0,0 +1,160 @@ +--- +title: Secondary Indexes +description: Indexes documentation +--- + +[Secondary indexes](https://redis.io/topics/indexes) are used to enable lookup operations based on native Redis structures. +Values are written to the according indexes on every save and are removed when objects are deleted or [expire](/redis/redis-repositories/expirations). + +## Simple Property Index + +Given the sample `Person` entity shown earlier, we can create an index for `firstname` by annotating the property with `@Indexed`, as shown in the following example: + +*Example 1. Annotation driven indexing* + +```java +@RedisHash("people") +public class Person { + + @Id String id; + @Indexed String firstname; + String lastname; + Address address; +} +``` + +Indexes are built up for actual property values. +Saving two Persons (for example, "rand" and "aviendha") results in setting up indexes similar to the following: + +```text +SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e +SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 +``` + +It is also possible to have indexes on nested elements. +Assume `Address` has a `city` property that is annotated with `@Indexed`. +In that case, once `person.address.city` is not `null`, we have Sets for each city, as shown in the following example: + +```text +SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e +``` + +Furthermore, the programmatic setup lets you define indexes on map keys and list properties, as shown in the following example: + +```java +@RedisHash("people") +public class Person { + + // ... other properties omitted + + Map attributes; // (1) + Map relatives; // (2) + List
addresses; // (3) +} +``` +```text +1. `SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e` +2. `SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e` +3. `SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e` +``` + +:::caution +Indexes cannot be resolved on [References](/redis/redis-repositories/usage#redis.repositories.references). +::: + +As with keyspaces, you can configure indexes without needing to annotate the actual domain type, as shown in the following example: + +*Example 2. Index Setup with @EnableRedisRepositories* + +```java +@Configuration +@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class) +public class ApplicationConfig { + + //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + + public static class MyIndexConfiguration extends IndexConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections.singleton(new SimpleIndexDefinition("people", "firstname")); + } + } +} +``` + +Again, as with keyspaces, you can programmatically configure indexes, as shown in the following example: + +*Example 3. Programmatic Index setup* + +```java +@Configuration +@EnableRedisRepositories +public class ApplicationConfig { + + //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + + @Bean + public RedisMappingContext keyValueMappingContext() { + return new RedisMappingContext( + new MappingConfiguration( + new KeyspaceConfiguration(), new MyIndexConfiguration())); + } + + public static class MyIndexConfiguration extends IndexConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections.singleton(new SimpleIndexDefinition("people", "firstname")); + } + } +} +``` + +## Geospatial Index + +Assume the `Address` type contains a `location` property of type `Point` that holds the geo coordinates of the particular address. +By annotating the property with `@GeoIndexed`, Spring Data Redis adds those values by using Redis `GEO` commands, as shown in the following example: + +```java +@RedisHash("people") +public class Person { + + Address address; + + // ... other properties omitted +} + +public class Address { + + @GeoIndexed Point location; + + // ... other properties omitted +} + +public interface PersonRepository extends CrudRepository { + + List findByAddressLocationNear(Point point, Distance distance); // (1) + List findByAddressLocationWithin(Circle circle); // (2) +} + +Person rand = new Person("rand", "al'thor"); +rand.setAddress(new Address(new Point(13.361389D, 38.115556D))); + +repository.save(rand); // (3) + +repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS)); // (4) +``` +```text +1. Query method declaration on a nested property, using `Point` and `Distance`. +2. Query method declaration on a nested property, using `Circle` to search within. +3. `GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e` +4. `GEORADIUS people:address:location 15.0 37.0 200.0 km` +``` + +In the preceding example the, longitude and latitude values are stored by using `GEOADD` that use the object's `id` as the member's name. +The finder methods allow usage of `Circle` or `Point, Distance` combinations for querying those values. + +:::note +It is **not** possible to combine `near` and `within` with other criteria. +::: diff --git a/docs/src/content/docs/redis/redis-repositories/keyspaces.md b/docs/src/content/docs/redis/redis-repositories/keyspaces.md new file mode 100644 index 000000000..da94cc3cc --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/keyspaces.md @@ -0,0 +1,57 @@ +--- +title: Keyspaces +description: Keyspaces documentation +--- + +Keyspaces define prefixes used to create the actual key for the Redis Hash. +By default, the prefix is set to `getClass().getName()`. +You can alter this default by setting `@RedisHash` on the aggregate root level or by setting up a programmatic configuration. +However, the annotated keyspace supersedes any other configuration. + +The following example shows how to set the keyspace configuration with the `@EnableRedisRepositories` annotation: + +*Example 1. Keyspace Setup via `@EnableRedisRepositories`* + +```java +@Configuration +@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class) +public class ApplicationConfig { + + //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + + public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections.singleton(new KeyspaceSettings(Person.class, "people")); + } + } +} +``` + +The following example shows how to programmatically set the keyspace: + +*Example 2. Programmatic Keyspace setup* + +```java +@Configuration +@EnableRedisRepositories +public class ApplicationConfig { + + //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + + @Bean + public RedisMappingContext keyValueMappingContext() { + return new RedisMappingContext( + new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration())); + } + + public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { + + @Override + protected Iterable initialConfiguration() { + return Collections.singleton(new KeyspaceSettings(Person.class, "people")); + } + } +} +``` diff --git a/docs/src/content/docs/redis/redis-repositories/mapping.md b/docs/src/content/docs/redis/redis-repositories/mapping.md new file mode 100644 index 000000000..04ce1985d --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/mapping.md @@ -0,0 +1,195 @@ +--- +title: Object-to-Hash Mapping +description: Mapping documentation +--- + +The Redis Repository support persists Objects to Hashes. +This requires an Object-to-Hash conversion which is done by a `RedisConverter`. +The default implementation uses `Converter` for mapping property values to and from Redis native `byte[]`. + +Given the `Person` type from the previous sections, the default mapping looks like the following: + +```text +_class = org.example.Person // (1) +id = e2c7dcee-b8cd-4424-883e-736ce564363e +firstname = rand // (2) +lastname = al'thor +address.city = emond's field // (3) +address.country = andor +``` +```text +1. The `_class` attribute is included on the root level as well as on any nested interface or abstract types. +2. Simple property values are mapped by path. +3. Properties of complex types are mapped by their dot path. +``` + +## Data Mapping and Type Conversion + +This section explains how types are mapped to and from a Hash representation: + +*Table 1. Default Mapping Rules* + +| Type | Sample | Mapped Value | +|------|--------|--------------| +| Simple Type (for example, String) | `String firstname = "rand";` | `firstname = "rand"` | +| Byte array (`byte[]`) | `byte[] image = "rand".getBytes();` | `image = "rand"` | +| Complex Type (for example, Address) | `Address address = new Address("emond's field");` | `address.city = "emond's field"` | +| List of Simple Type | `List nicknames = asList("dragon reborn", "lews therin");` | `nicknames.[0] = "dragon reborn"`, `nicknames.[1] = "lews therin"` | +| Map of Simple Type | `Map atts = asMap({"eye-color", "grey"}, {"hair-color", "brown"});` | `atts.[eye-color] = "grey"`, `atts.[hair-color] = "brown"` | +| List of Complex Type | `List
addresses = asList(new Address("emond's field"));` | `addresses.[0].city = "emond's field"`, `addresses.[1].city = "..."` | +| Map of Complex Type | `Map addresses = asMap({"home", new Address("emond's field")});` | `addresses.[home].city = "emond's field"`, `addresses.[work].city = "..."` | + +:::caution +Due to the flat representation structure, Map keys need to be simple types, such as `String` or `Number`. +::: + +Mapping behavior can be customized by registering the corresponding `Converter` in `RedisCustomConversions`. +Those converters can take care of converting from and to a single `byte[]` as well as `Map`. +The first one is suitable for (for example) converting a complex type to (for example) a binary JSON representation that still uses the default mappings hash structure. +The second option offers full control over the resulting hash. + +:::danger +Writing objects to a Redis hash deletes the content from the hash and re-creates the whole hash, so data that has not been mapped is lost. +::: + +The following example shows two sample byte array converters: + +*Example 1. Sample byte[] Converters* + +```java +@WritingConverter +public class AddressToBytesConverter implements Converter { + + private final Jackson2JsonRedisSerializer
serializer; + + public AddressToBytesConverter() { + + serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer.setObjectMapper(new ObjectMapper()); + } + + @Override + public byte[] convert(Address value) { + return serializer.serialize(value); + } +} + +@ReadingConverter +public class BytesToAddressConverter implements Converter { + + private final Jackson2JsonRedisSerializer
serializer; + + public BytesToAddressConverter() { + + serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer.setObjectMapper(new ObjectMapper()); + } + + @Override + public Address convert(byte[] value) { + return serializer.deserialize(value); + } +} +``` + +Using the preceding byte array `Converter` produces output similar to the following: + +```text +_class = org.example.Person +id = e2c7dcee-b8cd-4424-883e-736ce564363e +firstname = rand +lastname = al'thor +address = { city : "emond's field", country : "andor" } +``` + +The following example shows two examples of `Map` converters: + +*Example 2. Sample Map Converters* + +```java +@WritingConverter +public class AddressToMapConverter implements Converter> { + + @Override + public Map convert(Address source) { + return singletonMap("ciudad", source.getCity().getBytes()); + } +} + +@ReadingConverter +public class MapToAddressConverter implements Converter, Address> { + + @Override + public Address convert(Map source) { + return new Address(new String(source.get("ciudad"))); + } +} +``` + +Using the preceding Map `Converter` produces output similar to the following: + +```text +_class = org.example.Person +id = e2c7dcee-b8cd-4424-883e-736ce564363e +firstname = rand +lastname = al'thor +ciudad = "emond's field" +``` + +:::note +Custom conversions have no effect on index resolution. [Secondary Indexes](/redis/redis-repositories/indexes) are still created, even for custom converted types. +::: + +## Customizing Type Mapping + +If you want to avoid writing the entire Java class name as type information and would rather like to use a key, you can use the `@TypeAlias` annotation on the entity class being persisted. +If you need to customize the mapping even more, look at the [`TypeInformationMapper`](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/convert/TypeInformationMapper.html) interface. +An instance of that interface can be configured at the `DefaultRedisTypeMapper`, which can be configured on `MappingRedisConverter`. + +The following example shows how to define a type alias for an entity: + +*Example 3. Defining `@TypeAlias` for an entity* + +```java +@TypeAlias("pers") +class Person { + +} +``` + +The resulting document contains `pers` as the value in a `_class` field. + +### Configuring Custom Type Mapping + +The following example demonstrates how to configure a custom `RedisTypeMapper` in `MappingRedisConverter`: + +*Example 4. Configuring a custom `RedisTypeMapper` via Spring Java Config* + +```java +class CustomRedisTypeMapper extends DefaultRedisTypeMapper { + //implement custom type mapping here +} +``` + +```java +@Configuration +class SampleRedisConfiguration { + + @Bean + public MappingRedisConverter redisConverter(RedisMappingContext mappingContext, + RedisCustomConversions customConversions, ReferenceResolver referenceResolver) { + + MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver, + customTypeMapper()); + + mappingRedisConverter.setCustomConversions(customConversions); + + return mappingRedisConverter; + } + + @Bean + public RedisTypeMapper customTypeMapper() { + return new CustomRedisTypeMapper(); + } +} +``` diff --git a/docs/src/content/docs/redis/redis-repositories/queries.md b/docs/src/content/docs/redis/redis-repositories/queries.md new file mode 100644 index 000000000..c512465a6 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/queries.md @@ -0,0 +1,74 @@ +--- +title: Redis-specific Query Methods +description: Queries documentation +--- + +Query methods allow automatic derivation of simple finder queries from the method name, as shown in the following example: + +*Example 1. Sample Repository finder Method* + +```java +public interface PersonRepository extends CrudRepository { + + List findByFirstname(String firstname); +} +``` + +:::note +Please make sure properties used in finder methods are set up for indexing. +::: + +:::note +Query methods for Redis repositories support only queries for entities and collections of entities with paging. +::: + +Using derived query methods might not always be sufficient to model the queries to run. `RedisCallback` offers more control over the actual matching of index structures or even custom indexes. +To do so, provide a `RedisCallback` that returns a single or `Iterable` set of `id` values, as shown in the following example: + +*Example 2. Sample finder using RedisCallback* + +```java +String user = //... + +List sessionsByUser = template.find(new RedisCallback>() { + + public Set doInRedis(RedisConnection connection) throws DataAccessException { + return connection + .sMembers("sessions:securityContext.authentication.principal.username:" + user); + }}, RedisSession.class); +``` + +The following table provides an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to: + +*Table 1. Supported keywords inside method names* + +| Keyword | Sample | Redis snippet | +|---------|--------|---------------| +| `And` | `findByLastnameAndFirstname` | `SINTER …:firstname:rand …:lastname:al'thor` | +| `Or` | `findByLastnameOrFirstname` | `SUNION …:firstname:rand …:lastname:al'thor` | +| `Is, Equals` | `findByFirstname`, `findByFirstnameIs`, `findByFirstnameEquals` | `SINTER …:firstname:rand` | +| `IsTrue` | `FindByAliveIsTrue` | `SINTER …:alive:1` | +| `IsFalse` | `findByAliveIsFalse` | `SINTER …:alive:0` | +| `Top,First` | `findFirst10ByFirstname`,`findTop5ByFirstname` | | + +## Sorting Query Method results + +Redis repositories allow various approaches to define sorting order. +Redis itself does not support in-flight sorting when retrieving hashes or sets. +Therefore, Redis repository query methods construct a `Comparator` that is applied to the result before returning results as `List`. +Let's take a look at the following example: + +*Example 3. Sorting Query Results* + +```java +interface PersonRepository extends RedisRepository { + + List findByFirstnameOrderByAgeDesc(String firstname); // (1) + + List findByFirstname(String firstname, Sort sort); // (2) +} +``` +```text +1. Static sorting derived from method name. +2. Dynamic sorting using a method argument. +``` diff --git a/docs/src/content/docs/redis/redis-repositories/query-by-example.md b/docs/src/content/docs/redis/redis-repositories/query-by-example.md new file mode 100644 index 000000000..16c034501 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/query-by-example.md @@ -0,0 +1,239 @@ +--- +title: Query by Example +description: Query by Example (QBE) for Redis repositories including usage, matchers, and Redis-specific limitations +--- + +## Introduction + +This chapter provides an introduction to Query by Example and explains how to use it. + +Query by Example (QBE) is a user-friendly querying technique with a simple interface. +It allows dynamic query creation and does not require you to write queries that contain field names. +In fact, Query by Example does not require you to write queries by using store-specific query languages at all. + +:::note +This chapter explains the core concepts of Query by Example. +The information is pulled from the Spring Data Commons module. +Depending on your database, String matching support can be limited. +::: + +## Usage + +The Query by Example API consists of four parts: + +* Probe: The actual example of a domain object with populated fields. +* `ExampleMatcher`: The `ExampleMatcher` carries details on how to match particular fields. +It can be reused across multiple Examples. +* `Example`: An `Example` consists of the probe and the `ExampleMatcher`. +It is used to create the query. +* `FetchableFluentQuery`: A `FetchableFluentQuery` offers a fluent API, that allows further customization of a query derived from an `Example`. +Using the fluent API lets you specify ordering projection and result processing for your query. + +Query by Example is well suited for several use cases: + +* Querying your data store with a set of static or dynamic constraints. +* Frequent refactoring of the domain objects without worrying about breaking existing queries. +* Working independently of the underlying data store API. + +Query by Example also has several limitations: + +* No support for nested or grouped property constraints, such as `firstname = ?0 or (firstname = ?1 and lastname = ?2)`. +* Store-specific support on string matching. +Depending on your databases, String matching can support starts/contains/ends/regex for strings. +* Exact matching for other property types. + +Before getting started with Query by Example, you need to have a domain object. +To get started, create an interface for your repository, as shown in the following example: + +*Sample Person object* + +```java +public class Person { + + @Id + private String id; + private String firstname; + private String lastname; + private Address address; + + // … getters and setters omitted +} +``` + +The preceding example shows a simple domain object. +You can use it to create an `Example`. +By default, fields having `null` values are ignored, and strings are matched by using the store specific defaults. + +:::note +Inclusion of properties into a Query by Example criteria is based on nullability. +Properties using primitive types (`int`, `double`, …) are always included unless the `ExampleMatcher` ignores the property path. +::: + +Examples can be built by either using the `of` factory method or by using `ExampleMatcher`. `Example` is immutable. +The following listing shows a simple Example: + +*Example 1. Simple Example* + +```java +Person person = new Person(); // (1) +person.setFirstname("Dave"); // (2) + +Example example = Example.of(person); // (3) +``` +```text +1. Create a new instance of the domain object. +2. Set the properties to query. +3. Create the `Example`. +``` + +You can run the example queries by using repositories. +To do so, let your repository interface extend `QueryByExampleExecutor`. +The following listing shows an excerpt from the `QueryByExampleExecutor` interface: + +*The `QueryByExampleExecutor`* + +```java +public interface QueryByExampleExecutor { + + S findOne(Example example); + + Iterable findAll(Example example); + + // … more functionality omitted. +} +``` +## Example Matchers + +Examples are not limited to default settings. +You can specify your own defaults for string matching, null handling, and property-specific settings by using the `ExampleMatcher`, as shown in the following example: + +*Example 2. Example matcher with customized matching* + +```java +Person person = new Person(); // (1) +person.setFirstname("Dave"); // (2) + +ExampleMatcher matcher = ExampleMatcher.matching() // (3) + .withIgnorePaths("lastname") // (4) + .withIncludeNullValues() // (5) + .withStringMatcher(StringMatcher.ENDING); // (6) + +Example example = Example.of(person, matcher); // (7) +``` +```text +1. Create a new instance of the domain object. +2. Set properties. +3. Create an `ExampleMatcher` to expect all values to match. +It is usable at this stage even without further configuration. +4. Construct a new `ExampleMatcher` to ignore the `lastname` property path. +5. Construct a new `ExampleMatcher` to ignore the `lastname` property path and to include null values. +6. Construct a new `ExampleMatcher` to ignore the `lastname` property path, to include null values, and to perform suffix string matching. +7. Create a new `Example` based on the domain object and the configured `ExampleMatcher`. +``` + +By default, the `ExampleMatcher` expects all values set on the probe to match. +If you want to get results matching any of the predicates defined implicitly, use `ExampleMatcher.matchingAny()`. + +You can specify behavior for individual properties (such as "firstname" and "lastname" or, for nested properties, "address.city"). +You can tune it with matching options and case sensitivity, as shown in the following example: + +*Configuring matcher options* + +```java +ExampleMatcher matcher = ExampleMatcher.matching() + .withMatcher("firstname", endsWith()) + .withMatcher("lastname", startsWith().ignoreCase()); +} +``` + +Another way to configure matcher options is to use lambdas (introduced in Java 8). +This approach creates a callback that asks the implementor to modify the matcher. +You need not return the matcher, because configuration options are held within the matcher instance. +The following example shows a matcher that uses lambdas: + +*Configuring matcher options with lambdas* + +```java +ExampleMatcher matcher = ExampleMatcher.matching() + .withMatcher("firstname", match -> match.endsWith()) + .withMatcher("firstname", match -> match.startsWith()); +} +``` + +Queries created by `Example` use a merged view of the configuration. +Default matching settings can be set at the `ExampleMatcher` level, while individual settings can be applied to particular property paths. +Settings that are set on `ExampleMatcher` are inherited by property path settings unless they are defined explicitly. +Settings on a property patch have higher precedence than default settings. +The following table describes the scope of the various `ExampleMatcher` settings: + +*Table 1. Scope of `ExampleMatcher` settings* + +| Setting | Scope | +|---------|-------| +| Null-handling | `ExampleMatcher` | +| String matching | `ExampleMatcher` and property path | +| Ignoring properties | Property path | +| Case sensitivity | `ExampleMatcher` and property path | +| Value transformation | Property path | + +## Fluent API + +`QueryByExampleExecutor` offers one more method, which we did not mention so far: ` R findBy(Example example, Function, R> queryFunction)`. +As with other methods, it executes a query derived from an `Example`. +However, with the second argument, you can control aspects of that execution that you cannot dynamically control otherwise. +You do so by invoking the various methods of the `FetchableFluentQuery` in the second argument. +`sortBy` lets you specify an ordering for your result. +`as` lets you specify the type to which you want the result to be transformed. +`project` limits the queried attributes. +`first`, `firstValue`, `one`, `oneValue`, `all`, `page`, `slice`, `stream`, `count`, and `exists` define what kind of result you get and how the query behaves when more than the expected number of results are available. + +Use the fluent API to get the last of potentially many results, ordered by lastname: + +```java +Optional match = repository.findBy(example, + q -> q + .sortBy(Sort.by("lastname").descending()) + .first() +); +``` + +## Running an Example + +The following example uses Query by Example against a repository: + +*Example 3. Query by Example using a Repository* +```java +interface PersonRepository extends ListQueryByExampleExecutor { +} + +class PersonService { + + @Autowired PersonRepository personRepository; + + List findPeople(Person probe) { + return personRepository.findAll(Example.of(probe)); + } +} +``` + +Redis Repositories support, with their secondary indexes, a subset of Spring Data's Query by Example features. +In particular, only exact, case-sensitive, and non-null values are used to construct a query. + +Secondary indexes use set-based operations (Set intersection, Set union) to determine matching keys. Adding a property to the query that is not indexed returns no result, because no index exists. Query by Example support inspects indexing configuration to include only properties in the query that are covered by an index. This is to prevent accidental inclusion of non-indexed properties. + +Case-insensitive queries and unsupported `StringMatcher` instances are rejected at runtime. + +The following list shows the supported Query by Example options: + +* Case-sensitive, exact matching of simple and nested properties +* Any/All match modes +* Value transformation of the criteria value +* Exclusion of `null` values from the criteria + +The following list shows properties not supported by Query by Example: + +* Case-insensitive matching +* Regex, prefix/contains/suffix String-matching +* Querying of Associations, Collection, and Map-like properties +* Inclusion of `null` values from the criteria +* `findAll` with sorting diff --git a/docs/src/content/docs/redis/redis-repositories/usage.md b/docs/src/content/docs/redis/redis-repositories/usage.md new file mode 100644 index 000000000..d54e9d669 --- /dev/null +++ b/docs/src/content/docs/redis/redis-repositories/usage.md @@ -0,0 +1,158 @@ +--- +title: Usage +description: Usage documentation +--- + +Spring Data Redis lets you easily implement domain entities, as shown in the following example: + +*Example 1. Sample Person Entity* + +```java +@RedisHash("people") +public class Person { + + @Id String id; + String firstname; + String lastname; + Address address; +} +``` + +We have a pretty simple domain object here. +Note that it has a `@RedisHash` annotation on its type and a property named `id` that is annotated with `org.springframework.data.annotation.Id`. +Those two items are responsible for creating the actual key used to persist the hash. + +:::note +Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. +Those with the annotation are favored over others. +::: + +To now actually have a component responsible for storage and retrieval, we need to define a repository interface, as shown in the following example: + +*Example 2. Basic Repository Interface To Persist Person Entities* + +```java +public interface PersonRepository extends CrudRepository { + +} +``` + +As our repository extends `CrudRepository`, it provides basic CRUD and finder operations. +The thing we need in between to glue things together is the corresponding Spring configuration, shown in the following example: + +*Example 3. JavaConfig for Redis Repositories* + +```java +@Configuration +@EnableRedisRepositories +public class ApplicationConfig { + + @Bean + public RedisConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } +} +``` + +Given the preceding setup, we can inject `PersonRepository` into our components, as shown in the following example: + +*Example 4. Access to Person Entities* + +```java +@Autowired PersonRepository repo; + +public void basicCrudOperations() { + + Person rand = new Person("rand", "al'thor"); + rand.setAddress(new Address("emond's field", "andor")); + + repo.save(rand); // (1) + + repo.findOne(rand.getId()); // (2) + + repo.count(); // (3) + + repo.delete(rand); // (4) +} +``` +```text +1. Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Redis Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2025-beeb-c666fab4c0e5`. +2. Uses the provided `id` to retrieve the object stored at `keyspace:id`. +3. Counts the total number of entities available within the keyspace, `people`, defined by `@RedisHash` on `Person`. +4. Removes the key for the given object from Redis. +``` + +## Persisting References + +Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself. +On loading from Redis, references are resolved automatically and mapped back into the object, as shown in the following example: + +*Example 5. Sample Property Reference* + +```text +_class = org.example.Person +id = e2c7dcee-b8cd-4424-883e-736ce564363e +firstname = rand +lastname = al'thor +mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 // (1) +``` +```text +1. Reference stores the whole key (`keyspace:id`) of the referenced object. +``` + +:::danger +Referenced Objects are not persisted when the referencing object is saved. +You must persist changes on referenced objects separately, since only the reference is stored. +Indexes set on properties of referenced types are not resolved. +::: + +## Persisting Partial Updates + +In some cases, you need not load and rewrite the entire entity just to set a new value within it. +A session timestamp for the last active time might be such a scenario where you want to alter one property. +`PartialUpdate` lets you define `set` and `delete` actions on existing objects while taking care of updating potential expiration times of both the entity itself and index structures. +The following example shows a partial update: + +*Example 6. Sample Partial Update* + +```java +PartialUpdate update = new PartialUpdate("e2c7dcee", Person.class) + .set("firstname", "mat") // (1) + .set("address.city", "emond's field") // (2) + .del("age"); // (3) + +template.update(update); + +update = new PartialUpdate("e2c7dcee", Person.class) + .set("address", new Address("caemlyn", "andor")) // (4) + .set("attributes", singletonMap("eye-color", "grey")); // (5) + +template.update(update); + +update = new PartialUpdate("e2c7dcee", Person.class) + .refreshTtl(true); // (6) + .set("expiration", 1000); + +template.update(update); +``` +```text +1. Set the simple `firstname` property to `mat`. +2. Set the simple 'address.city' property to 'emond's field' without having to pass in the entire object. +This does not work when a custom conversion is registered. +3. Remove the `age` property. +4. Set complex `address` property. +5. Set a map of values, which removes the previously existing map and replaces the values with the given ones. +6. Automatically update the server expiration time when altering [Time To Live](/redis/redis-repositories/expirations). +``` + +:::note +Updating complex objects as well as map (or other collection) structures requires further interaction with Redis to determine existing values, which means that rewriting the entire entity might be faster. +::: diff --git a/docs/src/content/docs/redis/redis-streams.md b/docs/src/content/docs/redis/redis-streams.md new file mode 100644 index 000000000..e31ffb1bb --- /dev/null +++ b/docs/src/content/docs/redis/redis-streams.md @@ -0,0 +1,322 @@ +--- +title: Redis Streams +description: Redis Streams documentation +--- + +Redis Streams model a log data structure in an abstract approach. Typically, logs are append-only data structures and are consumed from the beginning on, at a random position, or by streaming new messages. + +:::note +Learn more about Redis Streams in the [Redis reference documentation](https://redis.io/topics/streams-intro). +::: + +Redis Streams can be roughly divided into two areas of functionality: + +* Appending records +* Consuming records + +Although this pattern has similarities to [Pub/Sub](/redis/pubsub), the main difference lies in the persistence of messages and how they are consumed. + +While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don't listen, you miss a message), Redis Stream use a persistent, append-only data type that retains messages until the stream is trimmed. Another difference in consumption is that Pub/Sub registers a server-side subscription. Redis pushes arriving messages to the client while Redis Streams require active polling. + +The `org.springframework.data.redis.connection` and `org.springframework.data.redis.stream` packages provide the core functionality for Redis Streams. + +## Appending + +To send a record, you can use, as with the other operations, either the low-level `RedisConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `RedisConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as records, as shown in the following example: + +```java +// append message through connection +RedisConnection con = … +byte[] stream = … +ByteRecord record = StreamRecords.rawBytes(…).withStreamKey(stream); +con.xAdd(record); + +// append message through RedisTemplate +RedisTemplate template = … +StringRecord record = StreamRecords.string(…).withStreamKey("my-stream"); +template.opsForStream().add(record); +``` + +Stream records carry a `Map`, key-value tuples, as their payload. Appending a record to a stream returns the `RecordId` that can be used as further reference. + +## Consuming + +On the consuming side, one can consume one or multiple streams. Redis Streams provide read commands that allow consumption of the stream from an arbitrary position (random access) within the known stream content and beyond the stream end to consume new stream record. + +At the low-level, `RedisConnection` offers the `xRead` and `xReadGroup` methods that map the Redis commands for reading and reading within a consumer group, respectively. Note that multiple streams can be used as arguments. + +:::note +Subscription commands in Redis can be blocking. That is, calling `xRead` on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the read command times out or receives a message. +::: + +To consume stream messages, one can either poll for messages in application code, or use one of the two [Asynchronous reception through Message Listener Containers](#asynchronous-reception-through-message-listener-containers), the imperative or the reactive one. Each time a new records arrives, the container notifies the application code. + +### Synchronous reception + +While stream consumption is typically associated with asynchronous processing, it is possible to consume messages synchronously. The overloaded `StreamOperations.read(…)` methods provide this functionality. During a synchronous receive, the calling thread potentially blocks until a message becomes available. The property `StreamReadOptions.block` specifies how long the receiver should wait before giving up waiting for a message. + +```java +// Read message through RedisTemplate +RedisTemplate template = … + +List> messages = template.opsForStream().read(StreamReadOptions.empty().count(2), + StreamOffset.latest("my-stream")); + +List> messages = template.opsForStream().read(Consumer.from("my-group", "my-consumer"), + StreamReadOptions.empty().count(2), + StreamOffset.create("my-stream", ReadOffset.lastConsumed())) +``` + +### Asynchronous reception through Message Listener Containers + +Due to its blocking nature, low-level polling is not attractive, as it requires connection and thread management for every single consumer. To alleviate this problem, Spring Data offers message listeners, which do all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). + +Spring Data ships with two implementations tailored to the used programming model: + +* `org.springframework.data.redis.stream.StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Redis Stream and drive the `org.springframework.data.redis.stream.StreamListener` instances that are injected into it. +* `org.springframework.data.redis.stream.StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Redis Stream as potentially infinite stream and emit stream messages through a `Flux`. + +`StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. + +Both containers allow runtime configuration changes so that you can add or remove subscriptions while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, it automatically performs a cleanup, and the thread is released. + +#### Imperative `StreamMessageListenerContainer` + +In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `org.springframework.data.redis.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. + +```java +class ExampleStreamListener implements StreamListener> { + + @Override + public void onMessage(MapRecord message) { + + System.out.println("MessageId: " + message.getId()); + System.out.println("Stream: " + message.getStream()); + System.out.println("Body: " + message.getValue()); + } +} +``` + +`StreamListener` represents a functional interface so implementations can be rewritten using their Lambda form: + +```java +message -> { + + System.out.println("MessageId: " + message.getId()); + System.out.println("Stream: " + message.getStream()); + System.out.println("Body: " + message.getValue()); +}; +``` + +Once you've implemented your `StreamListener`, it's time to create a message listener container and register a subscription: + +```java +RedisConnectionFactory connectionFactory = … +StreamListener> streamListener = … + +StreamMessageListenerContainerOptions> containerOptions = StreamMessageListenerContainerOptions + .builder().pollTimeout(Duration.ofMillis(100)).build(); + +StreamMessageListenerContainer> container = StreamMessageListenerContainer.create(connectionFactory, + containerOptions); + +Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), streamListener); +``` + +Please refer to the Javadoc of the various message listener containers for a full description of the features supported by each implementation. + +#### Reactive `StreamReceiver` + +Reactive consumption of streaming data sources typically happens through a `Flux` of events or messages. The reactive receiver implementation is provided with `StreamReceiver` and its overloaded `receive(…)` messages. The reactive approach requires fewer infrastructure resources such as threads in comparison to `StreamMessageListenerContainer` as it is leveraging threading resources provided by the driver. The receiving stream is a demand-driven publisher of `StreamMessage`: + +```java +Flux> messages = … + +return messages.doOnNext(it -> { + System.out.println("MessageId: " + message.getId()); + System.out.println("Stream: " + message.getStream()); + System.out.println("Body: " + message.getValue()); +}); +``` + +Now we need to create the `StreamReceiver` and register a subscription to consume stream messages: + +```java +ReactiveRedisConnectionFactory connectionFactory = … + +StreamReceiverOptions> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100)) + .build(); +StreamReceiver> receiver = StreamReceiver.create(connectionFactory, options); + +Flux> messages = receiver.receive(StreamOffset.fromStart("my-stream")); +``` + +Please refer to the Javadoc of the various message listener containers for a full description of the features supported by each implementation. + +:::note +Demand-driven consumption uses backpressure signals to activate and deactivate polling. `StreamReceiver` subscriptions pause polling if the demand is satisfied until subscribers signal further demand. Depending on the `ReadOffset` strategy, this can cause messages to be skipped. +::: + +### `Acknowledge` strategies + +When you read with messages via a `Consumer Group`, the server will remember that a given message was delivered and add it to the Pending Entries List (PEL). A list of messages delivered but not yet acknowledged. +Messages have to be acknowledged via `StreamOperations.acknowledge` in order to be removed from the Pending Entries List as shown in the snippet below. + +```java +StreamMessageListenerContainer> container = ... + +container.receive(Consumer.from("my-group", "my-consumer"), // (1) + StreamOffset.create("my-stream", ReadOffset.lastConsumed()), + msg -> { + + // ... + redisTemplate.opsForStream().acknowledge("my-group", msg); // (2) + }); +``` +```text +1. Read as _my-consumer_ from group _my-group_. Received messages are not acknowledged. +2. Acknowledged the message after processing. +``` + +:::tip +To auto acknowledge messages on receive use `receiveAutoAck` instead of `receive`. +::: + +### `ReadOffset` strategies + +Stream read operations accept a read offset specification to consume messages from the given offset on. `ReadOffset` represents the read offset specification. Redis supports 3 variants of offsets, depending on whether you consume the stream standalone or within a consumer group: + +* `ReadOffset.latest()` – Read the latest message. +* `ReadOffset.from(…)` – Read after a specific message Id. +* `ReadOffset.lastConsumed()` – Read after the last consumed message Id (consumer-group only). + +In the context of a message container-based consumption, we need to advance (or increment) the read offset when consuming a message. Advancing depends on the requested `ReadOffset` and consumption mode (with/without consumer groups). The following matrix explains how containers advance `ReadOffset`: + +*Table 1. ReadOffset Advancing* + +| Read offset | Standalone | Consumer Group | +|-------------|------------|----------------| +| Latest | Read latest message | Read latest message | +| Specific Message Id | Use last seen message as the next MessageId | Use last seen message as the next MessageId | +| Last Consumed | Use last seen message as the next MessageId | Last consumed message as per consumer group | + +Reading from a specific message id and the last consumed message can be considered safe operations that ensure consumption of all messages that were appended to the stream. +Using the latest message for read can skip messages that were added to the stream while the poll operation was in the state of dead time. Polling introduces a dead time in which messages can arrive between individual polling commands. Stream consumption is not a linear contiguous read but split into repeating `XREAD` calls. + +## Serialization + +Any Record sent to the stream needs to be serialized to its binary format. Due to the streams closeness to the hash data structure the stream key, field names and values use the according serializers configured on the `RedisTemplate`. + +*Table 2. Stream Serialization* + +| Stream Property | Serializer | Description | +|-----------------|------------|-------------| +| key | keySerializer | used for `Record#getStream()` | +| field | hashKeySerializer | used for each map key in the payload | +| value | hashValueSerializer | used for each map value in the payload | + +Please make sure to review `RedisSerializer`s in use and note that if you decide to not use any serializer you need to make sure those values are binary already. + +## Object Mapping + +### Simple Values + +`StreamOperations` allows to append simple values, via `ObjectRecord`, directly to the stream without having to put those values into a `Map` structure. +The value will then be assigned to an _payload_ field and can be extracted when reading back the value. + +```java +ObjectRecord record = StreamRecords.newRecord() + .in("my-stream") + .ofObject("my-value"); + +redisTemplate() + .opsForStream() + .add(record); // (1) + +List> records = redisTemplate() + .opsForStream() + .read(String.class, StreamOffset.fromStart("my-stream")); +``` +```text +1. XADD my-stream * "_class" "java.lang.String" "_raw" "my-value" +``` + +`ObjectRecord`s pass through the very same serialization process as the all other records, thus the Record can also obtained using the untyped read operation returning a `MapRecord`. + +### Complex Values + +Adding a complex value to the stream can be done in 3 ways: + +* Convert to simple value using e. g. a String JSON representation. +* Serialize the value with a suitable `RedisSerializer`. +* Convert the value into a `Map` suitable for serialization using a [`HashMapper`](/redis/hash-mappers). + +The first variant is the most straight forward one but neglects the field value capabilities offered by the stream structure, still the values in the stream will be readable for other consumers. +The 2nd option holds the same benefits as the first one, but may lead to a very specific consumer limitations as the all consumers must implement the very same serialization mechanism. +The `HashMapper` approach is the a bit more complex one making use of the steams hash structure, but flattening the source. Still other consumers remain able to read the records as long as suitable serializer combinations are chosen. + +:::note +[HashMappers](/redis/hash-mappers) convert the payload to a `Map` with specific types. Make sure to use Hash-Key and Hash-Value serializers that are capable of (de-)serializing the hash. +::: + +```java +ObjectRecord record = StreamRecords.newRecord() + .in("user-logon") + .ofObject(new User("night", "angel")); + +redisTemplate() + .opsForStream() + .add(record); // (1) + +List> records = redisTemplate() + .opsForStream() + .read(User.class, StreamOffset.fromStart("user-logon")); +``` +```text +1. XADD user-logon * "_class" "com.example.User" "firstname" "night" "lastname" "angel" +``` + +`StreamOperations` use by default [ObjectHashMapper](/redis/redis-repositories/mapping). +You may provide a `HashMapper` suitable for your requirements when obtaining `StreamOperations`. + +```java +redisTemplate() + .opsForStream(new Jackson2HashMapper(true)) + .add(record); // (1) +``` +```text +1. XADD user-logon * "firstname" "night" "@class" "com.example.User" "lastname" "angel" +``` + +:::note +A `StreamMessageListenerContainer` may not be aware of any `@TypeAlias` used on domain types as those need to be resolved through a `MappingContext`. +Make sure to initialize `RedisMappingContext` with a `initialEntitySet`. +::: + +```java +@Bean +RedisMappingContext redisMappingContext() { + RedisMappingContext ctx = new RedisMappingContext(); + ctx.setInitialEntitySet(Collections.singleton(Person.class)); + return ctx; +} + +@Bean +RedisConverter redisConverter(RedisMappingContext mappingContext) { + return new MappingRedisConverter(mappingContext); +} + +@Bean +ObjectHashMapper hashMapper(RedisConverter converter) { + return new ObjectHashMapper(converter); +} + +@Bean +StreamMessageListenerContainer streamMessageListenerContainer(RedisConnectionFactory connectionFactory, ObjectHashMapper hashMapper) { + StreamMessageListenerContainerOptions> options = StreamMessageListenerContainerOptions.builder() + .objectMapper(hashMapper) + .build(); + + return StreamMessageListenerContainer.create(connectionFactory, options); +} +``` diff --git a/docs/src/content/docs/redis/scripting.mdx b/docs/src/content/docs/redis/scripting.mdx new file mode 100644 index 000000000..501169a8a --- /dev/null +++ b/docs/src/content/docs/redis/scripting.mdx @@ -0,0 +1,82 @@ +--- +title: Scripting +description: Scripting documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Redis versions 2.6 and higher provide support for running Lua scripts through the [eval](https://redis.io/commands/eval) and [evalsha](https://redis.io/commands/evalsha) commands. Spring Data Redis provides a high-level abstraction for running scripts that handles serialization and automatically uses the Redis script cache. + +Scripts can be run by calling the `execute` methods of `RedisTemplate` and `ReactiveRedisTemplate`. Both use a configurable `org.springframework.data.redis.core.script.ScriptExecutor` (or `org.springframework.data.redis.core.script.ReactiveScriptExecutor`) to run the provided script. By default, the `org.springframework.data.redis.core.script.ScriptExecutor` (or `org.springframework.data.redis.core.script.ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. + +The default `org.springframework.data.redis.core.script.ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Redis script cache. + +The following example runs a common "check-and-set" scenario by using a Lua script. This is an ideal use case for a Redis script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. + +```java +@Bean +public RedisScript script() { + + ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")); + return RedisScript.of(scriptSource, Boolean.class); +} +``` + + + + +```java +public class Example { + + @Autowired + RedisOperations redisOperations; + + @Autowired + RedisScript script; + + public boolean checkAndSet(String expectedValue, String newValue) { + return redisOperations.execute(script, List.of("key"), expectedValue, newValue); + } +} +``` + + + + +```java +public class Example { + + @Autowired + ReactiveRedisOperations redisOperations; + + @Autowired + RedisScript script; + + public Flux checkAndSet(String expectedValue, String newValue) { + return redisOperations.execute(script, List.of("key"), expectedValue, newValue); + } +} +``` + + + + +```lua +-- checkandset.lua +local current = redis.call('GET', KEYS[1]) +if current == ARGV[1] + then redis.call('SET', KEYS[1], ARGV[2]) + return true +end +return false +``` + +The preceding code configures a `org.springframework.data.redis.core.script.RedisScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). + +:::tip +It is ideal to configure a single instance of `DefaultRedisScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. +::: + +The `checkAndSet` method above then runs the scripts. Scripts can be run within a `org.springframework.data.redis.core.SessionCallback` as part of a transaction or pipeline. See "[Redis Transactions](/redis/transactions)" and "[Pipelining](/redis/pipelining)" for more information. + +The scripting support provided by Spring Data Redis also lets you schedule Redis scripts for periodic running by using the Spring Task and Scheduler abstractions. See the [Spring Framework](https://spring.io/projects/spring-framework/) documentation for more details. diff --git a/docs/src/content/docs/redis/support-classes.mdx b/docs/src/content/docs/redis/support-classes.mdx new file mode 100644 index 000000000..335313172 --- /dev/null +++ b/docs/src/content/docs/redis/support-classes.mdx @@ -0,0 +1,72 @@ +--- +title: Support Classes +description: Support Classes documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Package `org.springframework.data.redis.support` offers various reusable components that rely on Redis as a backing store. +Currently, the package contains various JDK-based interface implementations on top of Redis, such as [atomic](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html) counters and JDK [Collections](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html). + +:::note +`org.springframework.data.redis.support.collections.RedisList` is forward-compatible with Java 21 `SequencedCollection`. +::: + +The atomic counters make it easy to wrap Redis key incrementation while the collections allow easy management of Redis keys with minimal storage exposure or API leakage. +In particular, the `org.springframework.data.redis.support.collections.RedisSet` and `org.springframework.data.redis.support.collections.RedisZSet` interfaces offer easy access to the set operations supported by Redis, such as `intersection` and `union`. `org.springframework.data.redis.support.collections.RedisList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. +The following example shows the configuration for a bean that uses a `org.springframework.data.redis.support.collections.RedisList`: + + + + +```java +@Configuration +class MyConfig { + + // … + + @Bean + RedisList stringRedisTemplate(RedisTemplate redisTemplate) { + return new DefaultRedisList<>(template, "queue-key"); + } +} +``` + + + + +```xml + + + + + + + + + +``` + + + + +The following example shows a Java configuration example for a `Deque`: + +```java +public class AnotherExample { + + // injected + private Deque queue; + + public void addTag(String tag) { + queue.push(tag); + } +} +``` + +As shown in the preceding example, the consuming code is decoupled from the actual storage implementation. +In fact, there is no indication that Redis is used underneath. +This makes moving from development to production environments transparent and highly increases testability (the Redis implementation can be replaced with an in-memory one). diff --git a/docs/src/content/docs/redis/template.mdx b/docs/src/content/docs/redis/template.mdx new file mode 100644 index 000000000..22fa94215 --- /dev/null +++ b/docs/src/content/docs/redis/template.mdx @@ -0,0 +1,341 @@ +--- +title: Working with Objects through RedisTemplate +description: Template documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Most users are likely to use `org.springframework.data.redis.core.RedisTemplate` and its corresponding package, `org.springframework.data.redis.core` or its reactive variant `org.springframework.data.redis.core.ReactiveRedisTemplate`. +The template is, in fact, the central class of the Redis module, due to its rich feature set. +The template offers a high-level abstraction for Redis interactions. +While `[Reactive]RedisConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. + +The `org.springframework.data.redis.core.RedisTemplate` class implements the `org.springframework.data.redis.core.RedisOperations` interface and its reactive variant `org.springframework.data.redis.core.ReactiveRedisTemplate` implements `org.springframework.data.redis.core.ReactiveRedisOperations`. + +:::note +The preferred way to reference operations on a `[Reactive]RedisTemplate` instance is through the +`[Reactive]RedisOperations` interface. +::: + +Moreover, the template provides operations views (following the grouping from the Redis command [reference](https://redis.io/commands)) that offer rich, generified interfaces for working against a certain type or certain key (through the `KeyBound` interfaces) as described in the following table: + +
+Operational views + + + + +| Interface | Description | +|-----------|-------------| +| *Key Type Operations* | | +| `org.springframework.data.redis.core.GeoOperations` | Redis geospatial operations, such as `GEOADD`, `GEORADIUS`,... | +| `org.springframework.data.redis.core.HashOperations` | Redis hash operations | +| `org.springframework.data.redis.core.HyperLogLogOperations` | Redis HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... | +| `org.springframework.data.redis.core.ListOperations` | Redis list operations | +| `org.springframework.data.redis.core.SetOperations` | Redis set operations | +| `org.springframework.data.redis.core.ValueOperations` | Redis string (or value) operations | +| `org.springframework.data.redis.core.ZSetOperations` | Redis zset (or sorted set) operations | +| *Key Bound Operations* | | +| `org.springframework.data.redis.core.BoundGeoOperations` | Redis key bound geospatial operations | +| `org.springframework.data.redis.core.BoundHashOperations` | Redis hash key bound operations | +| `org.springframework.data.redis.core.BoundKeyOperations` | Redis key bound operations | +| `org.springframework.data.redis.core.BoundListOperations` | Redis list key bound operations | +| `org.springframework.data.redis.core.BoundSetOperations` | Redis set key bound operations | +| `org.springframework.data.redis.core.BoundValueOperations` | Redis string (or value) key bound operations | +| `org.springframework.data.redis.core.BoundZSetOperations` | Redis zset (or sorted set) key bound operations | + + + + +| Interface | Description | +|-----------|-------------| +| *Key Type Operations* | | +| `org.springframework.data.redis.core.ReactiveGeoOperations` | Redis geospatial operations such as `GEOADD`, `GEORADIUS`, and others | +| `org.springframework.data.redis.core.ReactiveHashOperations` | Redis hash operations | +| `org.springframework.data.redis.core.ReactiveHyperLogLogOperations` | Redis HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) | +| `org.springframework.data.redis.core.ReactiveListOperations` | Redis list operations | +| `org.springframework.data.redis.core.ReactiveSetOperations` | Redis set operations | +| `org.springframework.data.redis.core.ReactiveValueOperations` | Redis string (or value) operations | +| `org.springframework.data.redis.core.ReactiveZSetOperations` | Redis zset (or sorted set) operations | + + + + +
+ +Once configured, the template is thread-safe and can be reused across multiple instances. + +`RedisTemplate` uses a Java-based serializer for most of its operations. +This means that any object written or read by the template is serialized and deserialized through Java. + +You can change the serialization mechanism on the template, and the Redis module offers several implementations, which are available in the `org.springframework.data.redis.serializer` package. +See [Serializers](#serializers) for more information. +You can also set any of the serializers to null and use RedisTemplate with raw byte arrays by setting the `enableDefaultSerializer` property to `false`. +Note that the template requires all keys to be non-null. +However, values can be null as long as the underlying serializer accepts them. +Read the Javadoc of each serializer for more information. + +For cases where you need a certain template view, declare the view as a dependency and inject the template. +The container automatically performs the conversion, eliminating the `opsFor[X]` calls, as shown in the following example: + +*Configuring Template API* + + + + +```java +@Configuration +class MyConfig { + + @Bean + LettuceConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + return template; + } +} +``` + + + + +```java +@Configuration +class MyConfig { + + @Bean + LettuceConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + ReactiveRedisTemplate ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) { + return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string()); + } +} +``` + + + + +```xml + + + + + + + ... + + +``` + + + + +*Pushing an item to a List using `[Reactive]RedisTemplate`* + + + + +```java +public class Example { + + // inject the actual operations + @Autowired + private RedisOperations operations; + + // inject the template as ListOperations + @Resource(name="redisTemplate") + private ListOperations listOps; + + public void addLink(String userId, URL url) { + listOps.leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +```java +public class Example { + + // inject the actual template + @Autowired + private ReactiveRedisOperations operations; + + public Mono addLink(String userId, URL url) { + return operations.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +## String-focused Convenience Classes + +Since it is quite common for the keys and values stored in Redis to be `java.lang.String`, the Redis modules provides two extensions to `RedisConnection` and `RedisTemplate`, respectively the `StringRedisConnection` (and its `DefaultStringRedisConnection` implementation) and `StringRedisTemplate` as a convenient one-stop solution for intensive String operations. +In addition to being bound to `String` keys, the template and the connection use the `StringRedisSerializer` underneath, which means the stored keys and values are human-readable (assuming the same encoding is used both in Redis and your code). +The following listings show an example: + + + + +```java +@Configuration +class RedisConfiguration { + + @Bean + LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } +} +``` + + + + +```java +@Configuration +class RedisConfiguration { + + @Bean + LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + ReactiveStringRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) { + return new ReactiveStringRedisTemplate<>(factory); + } +} +``` + + + + +```xml + + + + + + + + +``` + + + + + + + +```java +public class Example { + + @Autowired + private StringRedisTemplate redisTemplate; + + public void addLink(String userId, URL url) { + redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +```java +public class Example { + + @Autowired + private ReactiveStringRedisTemplate redisTemplate; + + public Mono addLink(String userId, URL url) { + return redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +As with the other Spring templates, `RedisTemplate` and `StringRedisTemplate` let you talk directly to Redis through the `RedisCallback` interface. +This feature gives complete control to you, as it talks directly to the `RedisConnection`. +Note that the callback receives an instance of `StringRedisConnection` when a `StringRedisTemplate` is used. +The following example shows how to use the `RedisCallback` interface: + +```java +public void useCallback() { + + redisOperations.execute(new RedisCallback() { + public Object doInRedis(RedisConnection connection) throws DataAccessException { + Long size = connection.dbSize(); + // Can cast to StringRedisConnection if using a StringRedisTemplate + ((StringRedisConnection)connection).set("key", "value"); + } + }); +} +``` + +## Serializers + +From the framework perspective, the data stored in Redis is only bytes. +While Redis itself supports various types, for the most part, these refer to the way the data is stored rather than what it represents. +It is up to the user to decide whether the information gets translated into strings or any other objects. + +In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Redis in the `org.springframework.data.redis.serializer` package. + +This package contains two types of serializers that, as the name implies, take care of the serialization process: + +* Two-way serializers based on `org.springframework.data.redis.serializer.RedisSerializer`. +* Element readers and writers that use `RedisElementReader` and ``RedisElementWriter``. + +The main difference between these variants is that `RedisSerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. + +Multiple implementations are available (including two that have been already mentioned in this documentation): + +* `org.springframework.data.redis.serializer.JdkSerializationRedisSerializer`, which is used by default for `org.springframework.data.redis.cache.RedisCache` and `org.springframework.data.redis.core.RedisTemplate`. +* the `StringRedisSerializer`. + +However, one can use `OxmSerializer` for Object/XML mapping through Spring [OXM](https://docs.spring.io/spring-framework/reference/data-access.html#oxm) support or `org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer` or `org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer` for storing data in [JSON](https://en.wikipedia.org/wiki/JSON) format. + +Do note that the storage format is not limited only to values. +It can be used for keys, values, or hashes without any restrictions. + +:::danger +By default, `org.springframework.data.redis.cache.RedisCache` and `org.springframework.data.redis.core.RedisTemplate` are configured to use Java native serialization. +Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. +Manipulated input could lead to unwanted code being run in the application during the deserialization step. +As a consequence, do not use serialization in untrusted environments. +In general, we strongly recommend any other message format (such as JSON) instead. + +If you are concerned about security vulnerabilities due to Java serialization, consider the general-purpose serialization filter mechanism at the core JVM level: + +* [Filter Incoming Serialization Data](https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html). +* [JEP 290](https://openjdk.org/jeps/290). +* [OWASP: Deserialization of untrusted data](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data). +::: diff --git a/docs/src/content/docs/redis/transactions.md b/docs/src/content/docs/redis/transactions.md new file mode 100644 index 000000000..d0df01e23 --- /dev/null +++ b/docs/src/content/docs/redis/transactions.md @@ -0,0 +1,117 @@ +--- +title: Redis Transactions +description: Transactions documentation +--- + +Redis provides support for [transactions](https://redis.io/topics/transactions) through the `multi`, `exec`, and `discard` commands. +These operations are available on `org.springframework.data.redis.core.RedisTemplate`. +However, `RedisTemplate` is not guaranteed to run all the operations in the transaction with the same connection. + +Spring Data Redis provides the `org.springframework.data.redis.core.SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Redis transactions.The following example uses the `multi` method: + +```java +//execute a transaction +List txResults = redisOperations.execute(new SessionCallback>() { + public List execute(RedisOperations operations) throws DataAccessException { + operations.multi(); + operations.opsForSet().add("key", "value1"); + + // This will contain the results of all operations in the transaction + return operations.exec(); + } +}); +System.out.println("Number of items added to set: " + txResults.get(0)); +``` + +`RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results of `exec` before returning. +There is an additional `exec` method that lets you pass a custom serializer for transaction results. + +It is worth mentioning that in case between `multi()` and `exec()` an exception happens (e.g. a timeout exception in case Redis does not respond within the timeout) then the connection may get stuck in a transactional state. +To prevent such a situation need have to discard the transactional state to clear the connection: + +```java +List txResults = redisOperations.execute(new SessionCallback>() { + public List execute(RedisOperations operations) throws DataAccessException { + boolean transactionStateIsActive = true; + try { + operations.multi(); + operations.opsForSet().add("key", "value1"); + + // This will contain the results of all operations in the transaction + return operations.exec(); + } catch (RuntimeException e) { + operations.discard(); + throw e; + } + } +}); +``` + +## `@Transactional` Support + +By default, `RedisTemplate` does not participate in managed Spring transactions. +If you want `RedisTemplate` to make use of Redis transaction when using `@Transactional` or `TransactionTemplate`, you need to be explicitly enable transaction support for each `RedisTemplate` by setting `setEnableTransactionSupport(true)`. +Enabling transaction support binds `RedisConnection` to the current transaction backed by a `ThreadLocal`. +If the transaction finishes without errors, the Redis transaction gets commited with `EXEC`, otherwise rolled back with `DISCARD`. +Redis transactions are batch-oriented. +Commands issued during an ongoing transaction are queued and only applied when committing the transaction. + +Spring Data Redis distinguishes between read-only and write commands in an ongoing transaction. +Read-only commands, such as `KEYS`, are piped to a fresh (non-thread-bound) `RedisConnection` to allow reads. +Write commands are queued by `RedisTemplate` and applied upon commit. + +The following example shows how to configure transaction management: + +*Example 1. Configuration enabling Transaction Management* + +```java +@Configuration +@EnableTransactionManagement // (1) +public class RedisTxContextConfiguration { + + @Bean + public StringRedisTemplate redisTemplate() { + StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory()); + // explicitly enable transaction support + template.setEnableTransactionSupport(true); // (2) + return template; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + // jedis || Lettuce + } + + @Bean + public PlatformTransactionManager transactionManager() throws SQLException { + return new DataSourceTransactionManager(dataSource()); // (3) + } + + @Bean + public DataSource dataSource() throws SQLException { + // ... + } +} +``` +```text +1. Configures a Spring Context to enable [declarative transaction management](https://docs.spring.io/spring-framework/reference/data-access.html#transaction-declarative). +2. Configures `RedisTemplate` to participate in transactions by binding connections to the current thread. +3. Transaction management requires a `PlatformTransactionManager`. +Spring Data Redis does not ship with a `PlatformTransactionManager` implementation. +Assuming your application uses JDBC, Spring Data Redis can participate in transactions by using existing transaction managers. +``` + +The following examples each demonstrate a usage constraint: + +*Example 2. Usage Constraints* + +```java +// must be performed on thread-bound connection +template.opsForValue().set("thing1", "thing2"); + +// read operation must be run on a free (not transaction-aware) connection +template.keys("*"); + +// returns null as values set within a transaction are not visible +template.opsForValue().get("thing1"); +``` diff --git a/docs/src/content/docs/repositories.md b/docs/src/content/docs/repositories.md new file mode 100644 index 000000000..1fc06f311 --- /dev/null +++ b/docs/src/content/docs/repositories.md @@ -0,0 +1,15 @@ +--- +title: Redis Repositories Overview +description: Redis Repositories with Spring Data +--- + +This chapter explains the basic foundations of Spring Data repositories and Redis specifics. +Before continuing to the Redis specifics, make sure you have a sound understanding of the basic concepts. + +The goal of the Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores. + +Working with Redis Repositories lets you seamlessly convert and store domain objects in Redis Hashes, apply custom mapping strategies, and use secondary indexes. + +:::note[Important] +Redis Repositories require at least Redis Server version 2.8.0 and do not work with transactions. Make sure to use a `RedisTemplate` with [disabled transaction support](/redis/transactions#tx.spring). +::: diff --git a/docs/src/content/docs/repositories/core-concepts.md b/docs/src/content/docs/repositories/core-concepts.md new file mode 100644 index 000000000..01ab808ee --- /dev/null +++ b/docs/src/content/docs/repositories/core-concepts.md @@ -0,0 +1,130 @@ +--- +title: Core concepts +description: Core concepts of Spring Data repositories including CrudRepository, PagingAndSortingRepository, and derived queries +--- + +The central interface in the Spring Data repository abstraction is `Repository`. +It takes the domain class to manage as well as the identifier type of the domain class as type arguments. +This interface acts primarily as a marker interface to capture the types to work with and to help you to discover interfaces that extend this one. + +:::tip +Spring Data considers domain types to be entities, more specifically aggregates. +So you will see the term "entity" used throughout the documentation that can be interchanged with the term "domain type" or "aggregate". + +As you might have noticed in the introduction it already hinted towards domain-driven concepts. +We consider domain objects in the sense of DDD. +Domain objects have identifiers (otherwise these would be identity-less value objects), and we somehow need to refer to identifiers when working with certain patterns to access data. +Referring to identifiers will become more meaningful as we talk about repositories and query methods. +::: + +The `CrudRepository` and `ListCrudRepository` interfaces provide sophisticated CRUD functionality for the entity class that is being managed. + +*`CrudRepository` Interface* + +```java +public interface CrudRepository extends Repository { + + S save(S entity); // (1) + + Optional findById(ID primaryKey); // (2) + + Iterable findAll(); // (3) + + long count(); // (4) + + void delete(T entity); // (5) + + boolean existsById(ID primaryKey); // (6) + + // … more functionality omitted. +} +``` +```text +1. Saves the given entity. +2. Returns the entity identified by the given ID. +3. Returns all entities. +4. Returns the number of entities. +5. Deletes the given entity. +6. Indicates whether an entity with the given ID exists. +``` + +The methods declared in this interface are commonly referred to as CRUD methods. +`ListCrudRepository` offers equivalent methods, but they return `List` where the `CrudRepository` methods return an `Iterable`. + +:::note[Important] +The repository interface implies a few reserved methods like `findById(ID identifier)` that target the domain type identifier property regardless of its property name. +Read more about this in "[Defining Query Methods](/repositories/query-methods-details#reserved-method-names)". + +You can annotate your query method with `@Query` to provide a custom query if a property named `Id` doesn't refer to the identifier. +Following that path can easily lead to confusion and is discouraged as you will quickly hit type limits if the `ID` type and the type of your `Id` property deviate. +::: + +:::note +We also provide persistence technology-specific abstractions, such as `JpaRepository` or `MongoRepository`. +Those interfaces extend `CrudRepository` and expose the capabilities of the underlying persistence technology in addition to the rather generic persistence technology-agnostic interfaces such as `CrudRepository`. +::: + +Additional to the `CrudRepository`, there are `PagingAndSortingRepository` and `ListPagingAndSortingRepository` which add additional methods to ease paginated access to entities: + +*`PagingAndSortingRepository` interface* + +```java +public interface PagingAndSortingRepository { + + Iterable findAll(Sort sort); + + Page findAll(Pageable pageable); +} +``` + +:::note +Extension interfaces are subject to be supported by the actual store module. +While this documentation explains the general scheme, make sure that your store module supports the interfaces that you want to use. +::: + +To access the second page of `User` by a page size of 20, you could do something like the following: + +```java +PagingAndSortingRepository repository = // … get access to a bean +Page users = repository.findAll(PageRequest.of(1, 20)); +``` + +`ListPagingAndSortingRepository` offers equivalent methods, but returns a `List` where the `PagingAndSortingRepository` methods return an `Iterable`. + +In addition to query methods, query derivation for both count and delete queries is available. +The following list shows the interface definition for a derived count query: + +*Derived Count Query* + +```java +interface UserRepository extends CrudRepository { + + long countByLastname(String lastname); +} +``` + +The following listing shows the interface definition for a derived delete query: + +*Derived Delete Query* + +```java +interface UserRepository extends CrudRepository { + + long deleteByLastname(String lastname); + + List removeByLastname(String lastname); +} +``` + +## Entity State Detection Strategies + +The following table describes the strategies that Spring Data offers for detecting whether an entity is new: + +*Table 1. Options for detection whether an entity is new in Spring Data* + +| | | +|-----------|-------------| +| `@Id`-Property inspection (the default) | By default, Spring Data inspects the identifier property of the given entity. If the identifier property is `null` or `0` in case of primitive types, then the entity is assumed to be new. Otherwise, it is assumed to not be new. | +| `@Version`-Property inspection | If a property annotated with `@Version` is present and `null`, or in case of a version property of primitive type `0` the entity is considered new. If the version property is present but has a different value, the entity is considered to not be new. If no version property is present Spring Data falls back to inspection of the identifier property. | +| Implementing `Persistable` | If an entity implements `Persistable`, Spring Data delegates the new detection to the `isNew(…)` method of the entity. See the [Javadoc](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Persistable.html) for details.

_Note: Properties of `Persistable` will get detected and persisted if you use `AccessType.PROPERTY`. To avoid that, use `@Transient`._
| +| Providing a custom `EntityInformation` implementation | You can customize the `EntityInformation` abstraction used in the repository base implementation by creating a subclass of the module specific repository factory and overriding the `getEntityInformation(…)` method. You then have to register the custom implementation of module specific repository factory as a Spring bean. Note that this should rarely be necessary. | diff --git a/docs/src/content/docs/repositories/core-domain-events.md b/docs/src/content/docs/repositories/core-domain-events.md new file mode 100644 index 000000000..ae2a19203 --- /dev/null +++ b/docs/src/content/docs/repositories/core-domain-events.md @@ -0,0 +1,39 @@ +--- +title: Publishing Events from Aggregate Roots +description: Domain event publishing from aggregate roots using @DomainEvents and @AfterDomainEventPublication +--- + +Entities managed by repositories are aggregate roots. +In a Domain-Driven Design application, these aggregate roots usually publish domain events. +Spring Data provides an annotation called `@DomainEvents` that you can use on a method of your aggregate root to make that publication as easy as possible, as shown in the following example: + +*Exposing domain events from an aggregate root* + +```java +class AnAggregateRoot { + + @DomainEvents // (1) + Collection domainEvents() { + // … return events you want to get published here + } + + @AfterDomainEventPublication // (2) + void callbackMethod() { + // … potentially clean up domain events list + } +} +``` +```text +1. The method that uses `@DomainEvents` can return either a single event instance or a collection of events. +It must not take any arguments. +2. After all events have been published, we have a method annotated with `@AfterDomainEventPublication`. +You can use it to potentially clean the list of events to be published (among other uses). +``` + +The methods are called every time one of the following a Spring Data repository methods are called: + +* `save(…)`, `saveAll(…)` +* `delete(…)`, `deleteAll(…)`, `deleteAllInBatch(…)`, `deleteInBatch(…)` + +Note, that these methods take the aggregate root instances as arguments. +This is why `deleteById(…)` is notably absent, as the implementations might choose to issue a query deleting the instance and thus we would never have access to the aggregate instance in the first place. diff --git a/docs/src/content/docs/repositories/core-extensions.md b/docs/src/content/docs/repositories/core-extensions.md new file mode 100644 index 000000000..1ec1f3c9d --- /dev/null +++ b/docs/src/content/docs/repositories/core-extensions.md @@ -0,0 +1,8 @@ +--- +title: Core Extensions +description: Core Extensions documentation +--- + +## Querydsl + +Spring Data Redis does not support Querydsl. diff --git a/docs/src/content/docs/repositories/create-instances.mdx b/docs/src/content/docs/repositories/create-instances.mdx new file mode 100644 index 000000000..9132f8f3d --- /dev/null +++ b/docs/src/content/docs/repositories/create-instances.mdx @@ -0,0 +1,115 @@ +--- +title: Creating Repository Instances +description: Guide to creating repository instances using Java configuration, XML configuration, filters, and standalone usage +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +This section covers how to create instances and bean definitions for the defined repository interfaces. + +## Java Configuration + +Use the store-specific `@Enable{store}Repositories` annotation on a Java configuration class to define a configuration for repository activation. +For an introduction to Java-based configuration of the Spring container, see [JavaConfig in the Spring reference documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java). + +A sample configuration to enable Spring Data repositories resembles the following: + +*Sample annotation-based repository configuration* + +```java +@Configuration +@EnableJpaRepositories("com.acme.repositories") +class ApplicationConfiguration { + + @Bean + EntityManagerFactory entityManagerFactory() { + // … + } +} +``` + +:::note +The preceding example uses the JPA-specific annotation, which you would change according to the store module you actually use. The same applies to the definition of the `EntityManagerFactory` bean. See the sections covering the store-specific configuration. +::: + +## XML Configuration + +Each Spring Data module includes a `repositories` element that lets you define a base package that Spring scans for you, as shown in the following example: + +*Enabling Spring Data repositories via XML* + +```xml + + + + + + +``` + +In the preceding example, Spring is instructed to scan `com.acme.repositories` and all its sub-packages for interfaces extending `Repository` or one of its sub-interfaces. +For each interface found, the infrastructure registers the persistence technology-specific `FactoryBean` to create the appropriate proxies that handle invocations of the query methods. +Each bean is registered under a bean name that is derived from the interface name, so an interface of `UserRepository` would be registered under `userRepository`. +Bean names for nested repository interfaces are prefixed with their enclosing type name. +The base package attribute allows wildcards so that you can define a pattern of scanned packages. + +## Using Filters + +By default, the infrastructure picks up every interface that extends the persistence technology-specific `Repository` sub-interface located under the configured base package and creates a bean instance for it. +However, you might want more fine-grained control over which interfaces have bean instances created for them. +To do so, use filter elements inside the repository declaration. +The semantics are exactly equivalent to the elements in Spring's component filters. +For details, see the [Spring reference documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-classpath-scanning) for these elements. + +For example, to exclude certain interfaces from instantiation as repository beans, you could use the following configuration: + +*Example 1. Using filters* + + + + +```java +@Configuration +@Enable{store}Repositories(basePackages = "com.acme.repositories", + includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") }, + excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") }) +class ApplicationConfiguration { + + @Bean + EntityManagerFactory entityManagerFactory() { + // … + } +} +``` + + + + +```xml + + + + +``` + + + + +The preceding example includes all interfaces ending with `SomeRepository` and excludes those ending with `SomeOtherRepository` from being instantiated. + +## Standalone Usage + +You can also use the repository infrastructure outside of a Spring container -- for example, in CDI environments.You still need some Spring libraries in your classpath, but, generally, you can set up repositories programmatically as well.The Spring Data modules that provide repository support ship with a persistence technology-specific `RepositoryFactory` that you can use, as follows: + +*Standalone usage of the repository factory* + +```java +RepositoryFactorySupport factory = … // Instantiate factory here +UserRepository repository = factory.getRepository(UserRepository.class); +``` diff --git a/docs/src/content/docs/repositories/custom-implementations.mdx b/docs/src/content/docs/repositories/custom-implementations.mdx new file mode 100644 index 000000000..9e7ecd8a4 --- /dev/null +++ b/docs/src/content/docs/repositories/custom-implementations.mdx @@ -0,0 +1,464 @@ +--- +title: Custom Repository Implementations +description: Guide to creating custom repository implementations including individual customizations, fragments, and base repository customization +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Spring Data provides various options to create query methods with little coding. +But when those options don't fit your needs you can also provide your own custom implementation for repository methods. +This section describes how to do that. + +## Customizing Individual Repositories + +To enrich a repository with custom functionality, you must first define a fragment interface and an implementation for the custom functionality, as follows: + +Interface for custom repository functionality: + +*Interface for custom repository functionality* + +```java +interface CustomizedUserRepository { + void someCustomMethod(User user); +} +``` + +Implementation of custom repository functionality: + +*Implementation of custom repository functionality* + +```java +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + @Override + public void someCustomMethod(User user) { + // Your custom implementation + } +} +``` + +:::note +The most important part of the class name that corresponds to the fragment interface is the `Impl` postfix. +You can customize the store-specific postfix by setting `@EnableRepositories(repositoryImplementationPostfix = …)`. +::: + +:::danger +Historically, Spring Data custom repository implementation discovery followed a [naming pattern](https://docs.spring.io/spring-data/commons/docs/1.9.0.RELEASE/reference/html/#repositories.single-repository-behaviour) that derived the custom implementation class name from the repository allowing effectively a single custom implementation. + +A type located in the same package as the repository interface, matching _repository interface name_ followed by _implementation postfix_, is considered a custom implementation and will be treated as a custom implementation. +A class following that name can lead to undesired behavior. + +We consider the single-custom implementation naming deprecated and recommend not using this pattern. +Instead, migrate to a fragment-based programming model. +::: + +The implementation itself does not depend on Spring Data and can be a regular Spring bean. +Consequently, you can use standard dependency injection behavior to inject references to other beans (such as a `JdbcTemplate`), take part in aspects, and so on. + +Then you can let your repository interface extend the fragment interface, as follows: + +*Changes to your repository interface* + +```java +interface UserRepository extends CrudRepository, CustomizedUserRepository { + + // Declare query methods here +} +``` + +Extending the fragment interface with your repository interface combines the CRUD and custom functionality and makes it available to clients. + +Spring Data repositories are implemented by using fragments that form a repository composition. +Fragments are the base repository, functional aspects (such as [Querydsl](/repositories/core-extensions#querydsl)), and custom interfaces along with their implementations. +Each time you add an interface to your repository interface, you enhance the composition by adding a fragment. +The base repository and repository aspect implementations are provided by each Spring Data module. + +The following example shows custom interfaces and their implementations: + +*Fragments with their implementations* + +```java +interface HumanRepository { + void someHumanMethod(User user); +} + +class HumanRepositoryImpl implements HumanRepository { + + @Override + public void someHumanMethod(User user) { + // Your custom implementation + } +} + +interface ContactRepository { + + void someContactMethod(User user); + + User anotherContactMethod(User user); +} + +class ContactRepositoryImpl implements ContactRepository { + + @Override + public void someContactMethod(User user) { + // Your custom implementation + } + + @Override + public User anotherContactMethod(User user) { + // Your custom implementation + } +} +``` + +The following example shows the interface for a custom repository that extends `CrudRepository`: + +*Changes to your repository interface* + +```java +interface UserRepository extends CrudRepository, HumanRepository, ContactRepository { + + // Declare query methods here +} +``` + +Repositories may be composed of multiple custom implementations that are imported in the order of their declaration. +Custom implementations have a higher priority than the base implementation and repository aspects. +This ordering lets you override base repository and aspect methods and resolves ambiguity if two fragments contribute the same method signature. +Repository fragments are not limited to use in a single repository interface. +Multiple repositories may use a fragment interface, letting you reuse customizations across different repositories. + +The following example shows a repository fragment and its implementation: + +Fragments overriding `save(…)`: + +```java +interface CustomizedSave { + S save(S entity); +} + +class CustomizedSaveImpl implements CustomizedSave { + + @Override + public S save(S entity) { + // Your custom implementation + } +} +``` + +The following example shows a repository that uses the preceding repository fragment: + +*Customized repository interfaces* + +```java +interface UserRepository extends CrudRepository, CustomizedSave { +} + +interface PersonRepository extends CrudRepository, CustomizedSave { +} +``` + +### Configuration + +The repository infrastructure tries to autodetect custom implementation fragments by scanning for classes below the package in which it found a repository. +These classes need to follow the naming convention of appending a postfix defaulting to `Impl`. + +The following example shows a repository that uses the default postfix and a repository that sets a custom value for the postfix: + +*Example 1. Configuration example* + + + + +```java +@Enable{store}Repositories(repositoryImplementationPostfix = "MyPostfix") +class Configuration { … } +``` + + + + +```xml + + + +``` + + + + +The first configuration in the preceding example tries to look up a class called `com.acme.repository.CustomizedUserRepositoryImpl` to act as a custom repository implementation. +The second example tries to look up `com.acme.repository.CustomizedUserRepositoryMyPostfix`. + +#### Resolution of Ambiguity + +If multiple implementations with matching class names are found in different packages, Spring Data uses the bean names to identify which one to use. + +Given the following two custom implementations for the `CustomizedUserRepository` shown earlier, the first implementation is used. +Its bean name is `customizedUserRepositoryImpl`, which matches that of the fragment interface (`CustomizedUserRepository`) plus the postfix `Impl`. + +*Example 2. Resolution of ambiguous implementations* + +```java +package com.acme.impl.one; + +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + // Your custom implementation +} +``` + +```java +package com.acme.impl.two; + +@Component("specialCustomImpl") +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + // Your custom implementation +} +``` + +If you annotate the `UserRepository` interface with `@Component("specialCustom")`, the bean name plus `Impl` then matches the one defined for the repository implementation in `com.acme.impl.two`, and it is used instead of the first one. + +#### Manual Wiring + +If your custom implementation uses annotation-based configuration and autowiring only, the preceding approach shown works well, because it is treated as any other Spring bean. +If your implementation fragment bean needs special wiring, you can declare the bean and name it according to the conventions described in the [preceding section](#resolution-of-ambiguity). +The infrastructure then refers to the manually defined bean definition by name instead of creating one itself. +The following example shows how to manually wire a custom implementation: + +*Example 3. Manual wiring of custom implementations* + + + + +```java +class MyClass { + MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) { + … + } +} +``` + + + + +```xml + + + + + +``` + + + + +#### Registering Fragments with spring.factories + +As already mentioned in the [Configuration](#configuration) section, the infrastructure only auto-detects fragments within the repository base-package. +Therefore, fragments residing in another location or want to be contributed by an external archive will not be found if they do not share a common namespace. +Registering fragments within `spring.factories` allows you to circumvent this restriction as explained in the following section. + +Imagine you'd like to provide some custom search functionality usable across multiple repositories for your organization leveraging a text search index. + +First all you need is the fragment interface. +Note the generic `` parameter to align the fragment with the repository domain type. + +*Fragment Interface* + +```java +package com.acme.search; + +public interface SearchExtension { + + List search(String text, Limit limit); +} +``` + +Let's assume the actual full-text search is available via a `SearchService` that is registered as a `Bean` within the context so you can consume it in our `SearchExtension` implementation. +All you need to run the search is the collection (or index) name and an object mapper that converts the search results into actual domain objects as sketched out below. + +Fragment implementation: + +```java +package com.acme.search; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Limit; +import org.springframework.data.repository.core.RepositoryMethodContext; + +class DefaultSearchExtension implements SearchExtension { + + private final SearchService service; + + DefaultSearchExtension(SearchService service) { + this.service = service; + } + + @Override + public List search(String text, Limit limit) { + return search(RepositoryMethodContext.getContext(), text, limit); + } + + List search(RepositoryMethodContext metadata, String text, Limit limit) { + + Class domainType = metadata.getRepository().getDomainType(); + + String indexName = domainType.getSimpleName().toLowerCase(); + List jsonResult = service.search(indexName, text, 0, limit.max()); + + return jsonResult.stream().map(…).collect(toList()); + } +} +``` + +In the example above `RepositoryMethodContext.getContext()` is used to retrieve metadata for the actual method invocation. +`RepositoryMethodContext` exposes information attached to the repository such as the domain type. +In this case we use the repository domain type to identify the name of the index to be searched. + +Exposing invocation metadata is costly, hence it is disabled by default. +To access `RepositoryMethodContext.getContext()` you need to advise the repository factory responsible for creating the actual repository to expose method metadata. + +*Example 4. Expose Repository Metadata* + + + + +Adding the `RepositoryMetadataAccess` marker interface to the fragments implementation will trigger the infrastructure and enable metadata exposure for those repositories using the fragment. + +```java +package com.acme.search; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Limit; +import org.springframework.data.repository.core.support.RepositoryMetadataAccess; +import org.springframework.data.repository.core.RepositoryMethodContext; + +class DefaultSearchExtension implements SearchExtension, RepositoryMetadataAccess { + + // ... +} +``` + + + + +The `exposeMetadata` flag can be set directly on the repository factory bean via a `BeanPostProcessor`. + +```java +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.lang.Nullable; + +@Configuration +class MyConfiguration { + + @Bean + static BeanPostProcessor exposeMethodMetadata() { + + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + + if(bean instanceof RepositoryFactoryBeanSupport factoryBean) { + factoryBean.setExposeMetadata(true); + } + return bean; + } + }; + } +} +``` + + + + +```text +Please do not just copy/paste the above but consider your actual use case which may require a more fine-grained approach as the above will simply enable the flag on every repository. +``` + +Having both, the fragment declaration and implementation in place you can register the extension in the `META-INF/spring.factories` file and package things up if needed. + +*Register the fragment in `META-INF/spring.factories`* + +```properties +com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension +``` + +Now you are ready to make use of your extension; Simply add the interface to your repository. + +*Using it* + +```java +package io.my.movies; + +import com.acme.search.SearchExtension; +import org.springframework.data.repository.CrudRepository; + +interface MovieRepository extends CrudRepository, SearchExtension { + +} +``` + +## Customize the Base Repository + +The approach described in the [preceding section](#manual-wiring) requires customization of each repository interfaces when you want to customize the base repository behavior so that all repositories are affected. +To instead change behavior for all repositories, you can create an implementation that extends the persistence technology-specific repository base class. +This class then acts as a custom base class for the repository proxies, as shown in the following example: + +*Custom repository base class* + +```java +class MyRepositoryImpl + extends SimpleJpaRepository { + + private final EntityManager entityManager; + + MyRepositoryImpl(JpaEntityInformation entityInformation, + EntityManager entityManager) { + super(entityInformation, entityManager); + + // Keep the EntityManager around to used from the newly introduced methods. + this.entityManager = entityManager; + } + + @Override + @Transactional + public S save(S entity) { + // implementation goes here + } +} +``` + +:::caution +The class needs to have a constructor of the super class which the store-specific repository factory implementation uses. +If the repository base class has multiple constructors, override the one taking an `EntityInformation` plus a store specific infrastructure object (such as an `EntityManager` or a template class). +::: + +The final step is to make the Spring Data infrastructure aware of the customized repository base class. +In configuration, you can do so by using the `repositoryBaseClass`, as shown in the following example: + +*Example 5. Configuring a custom repository base class* + + + + +```java +@Configuration +@Enable{store}Repositories(repositoryBaseClass = MyRepositoryImpl.class) +class ApplicationConfiguration { … } +``` + + + + +```xml + +``` + + + diff --git a/docs/src/content/docs/repositories/definition.md b/docs/src/content/docs/repositories/definition.md new file mode 100644 index 000000000..40e3d7c4f --- /dev/null +++ b/docs/src/content/docs/repositories/definition.md @@ -0,0 +1,163 @@ +--- +title: Defining Repository Interfaces +description: Guide to defining repository interfaces including fine-tuning, multiple Spring Data modules, and configuration +--- + +To define a repository interface, you first need to define a domain class-specific repository interface. +The interface must extend `Repository` and be typed to the domain class and an ID type. +If you want to expose CRUD methods for that domain type, you may extend `CrudRepository`, or one of its variants instead of `Repository`. + +## Fine-tuning Repository Definition + +There are a few variants how you can get started with your repository interface. + +The typical approach is to extend `CrudRepository`, which gives you methods for CRUD functionality. +CRUD stands for Create, Read, Update, Delete. +With version 3.0 we also introduced `ListCrudRepository` which is very similar to the `CrudRepository` but for those methods that return multiple entities it returns a `List` instead of an `Iterable` which you might find easier to use. + +If you are using a reactive store you might choose `ReactiveCrudRepository`, or `RxJava3CrudRepository` depending on which reactive framework you are using. + +If you are using Kotlin you might pick `CoroutineCrudRepository` which utilizes Kotlin's coroutines. + +Additionally you can extend `PagingAndSortingRepository`, `ReactiveSortingRepository`, `RxJava3SortingRepository`, or `CoroutineSortingRepository` if you need methods that allow to specify a `Sort` abstraction or in the first case a `Pageable` abstraction. +Note that the various sorting repositories no longer extended their respective CRUD repository as they did in Spring Data Versions pre 3.0. +Therefore, you need to extend both interfaces if you want functionality of both. + +If you do not want to extend Spring Data interfaces, you can also annotate your repository interface with `@RepositoryDefinition`. +Extending one of the CRUD repository interfaces exposes a complete set of methods to manipulate your entities. +If you prefer to be selective about the methods being exposed, copy the methods you want to expose from the CRUD repository into your domain repository. +When doing so, you may change the return type of methods. +Spring Data will honor the return type if possible. +For example, for methods returning multiple entities you may choose `Iterable`, `List`, `Collection` or a VAVR list. + +If many repositories in your application should have the same set of methods you can define your own base interface to inherit from. +Such an interface must be annotated with `@NoRepositoryBean`. +This prevents Spring Data to try to create an instance of it directly and failing because it can't determine the entity for that repository, since it still contains a generic type variable. + +The following example shows how to selectively expose CRUD methods (`findById` and `save`, in this case): + +*Selectively exposing CRUD methods* + +```java +@NoRepositoryBean +interface MyBaseRepository extends Repository { + + Optional findById(ID id); + + S save(S entity); +} + +interface UserRepository extends MyBaseRepository { + User findByEmailAddress(EmailAddress emailAddress); +} +``` + +In the prior example, you defined a common base interface for all your domain repositories and exposed `findById(…)` as well as `save(…)`.These methods are routed into the base repository implementation of the store of your choice provided by Spring Data (for example, if you use JPA, the implementation is `SimpleJpaRepository`), because they match the method signatures in `CrudRepository`. +So the `UserRepository` can now save users, find individual users by ID, and trigger a query to find `Users` by email address. + +:::note +The intermediate repository interface is annotated with `@NoRepositoryBean`. +Make sure you add that annotation to all repository interfaces for which Spring Data should not create instances at runtime. +::: + +## Using Repositories with Multiple Spring Data Modules + +Using a unique Spring Data module in your application makes things simple, because all repository interfaces in the defined scope are bound to the Spring Data module. +Sometimes, applications require using more than one Spring Data module. +In such cases, a repository definition must distinguish between persistence technologies. +When it detects multiple repository factories on the class path, Spring Data enters strict repository configuration mode. +Strict configuration uses details on the repository or the domain class to decide about Spring Data module binding for a repository definition: + +1. If the repository definition extends the module-specific repository, it is a valid candidate for the particular Spring Data module. +2. If the domain class is annotated with the module-specific type annotation, it is a valid candidate for the particular Spring Data module. +Spring Data modules accept either third-party annotations (such as JPA's `@Entity`) or provide their own annotations (such as `@Document` for Spring Data MongoDB and Spring Data Elasticsearch). + +The following example shows a repository that uses module-specific interfaces (JPA in this case): + +*Example 1. Repository definitions using module-specific interfaces* + +```java +interface MyRepository extends JpaRepository { } + +@NoRepositoryBean +interface MyBaseRepository extends JpaRepository { … } + +interface UserRepository extends MyBaseRepository { … } +``` + +`MyRepository` and `UserRepository` extend `JpaRepository` in their type hierarchy. +They are valid candidates for the Spring Data JPA module. + +The following example shows a repository that uses generic interfaces: + +*Example 2. Repository definitions using generic interfaces* + +```java +interface AmbiguousRepository extends Repository { … } + +@NoRepositoryBean +interface MyBaseRepository extends CrudRepository { … } + +interface AmbiguousUserRepository extends MyBaseRepository { … } +``` +```text +`AmbiguousRepository` and `AmbiguousUserRepository` extend only `Repository` and `CrudRepository` in their type hierarchy. +While this is fine when using a unique Spring Data module, multiple modules cannot distinguish to which particular Spring Data these repositories should be bound. +``` + +The following example shows a repository that uses domain classes with annotations: + +*Example 3. Repository definitions using domain classes with annotations* + +```java +interface PersonRepository extends Repository { … } + +@Entity +class Person { … } + +interface UserRepository extends Repository { … } + +@Document +class User { … } +``` + +`PersonRepository` references `Person`, which is annotated with the JPA `@Entity` annotation, so this repository clearly belongs to Spring Data JPA. `UserRepository` references `User`, which is annotated with Spring Data MongoDB's `@Document` annotation. + +The following bad example shows a repository that uses domain classes with mixed annotations: + +*Example 4. Repository definitions using domain classes with mixed annotations* + +```java +interface JpaPersonRepository extends Repository { … } + +interface MongoDBPersonRepository extends Repository { … } + +@Entity +@Document +class Person { … } +``` +```text +This example shows a domain class using both JPA and Spring Data MongoDB annotations. +It defines two repositories, `JpaPersonRepository` and `MongoDBPersonRepository`. +One is intended for JPA and the other for MongoDB usage. +Spring Data is no longer able to tell the repositories apart, which leads to undefined behavior. +``` + +Repository type details and distinguishing domain class annotations are used for strict repository configuration to identify repository candidates for a particular Spring Data module. +Using multiple persistence technology-specific annotations on the same domain type is possible and enables reuse of domain types across multiple persistence technologies. +However, Spring Data can then no longer determine a unique module with which to bind the repository. + +The last way to distinguish repositories is by scoping repository base packages. +Base packages define the starting points for scanning for repository interface definitions, which implies having repository definitions located in the appropriate packages. +By default, annotation-driven configuration uses the package of the configuration class. +The [base package in XML-based configuration](/repositories/create-instances#xml-configuration) is mandatory. + +The following example shows annotation-driven configuration of base packages: + +*Annotation-driven configuration of base packages* + +```java +@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa") +@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo") +class Configuration { … } +``` diff --git a/docs/src/content/docs/repositories/null-handling.md b/docs/src/content/docs/repositories/null-handling.md new file mode 100644 index 000000000..8fdce2cf4 --- /dev/null +++ b/docs/src/content/docs/repositories/null-handling.md @@ -0,0 +1,95 @@ +--- +title: Null Handling of Repository Methods +description: Null handling in Spring Data repositories including nullability annotations and Kotlin support +--- + +As of Spring Data 2.0, repository CRUD methods that return an individual aggregate instance use Java 8's `Optional` to indicate the potential absence of a value. +Besides that, Spring Data supports returning the following wrapper types on query methods: + +* `com.google.common.base.Optional` +* `scala.Option` +* `io.vavr.control.Option` + +Alternatively, query methods can choose not to use a wrapper type at all. +The absence of a query result is then indicated by returning `null`. +Repository methods returning collections, collection alternatives, wrappers, and streams are guaranteed never to return `null` but rather the corresponding empty representation. +See "[Repository query return types](/repositories/query-return-types-reference)" for details. + +## Nullability Annotations + +You can express nullability constraints for repository methods by using [Spring Framework's nullability annotations](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#null-safety). +They provide a tooling-friendly approach and opt-in `null` checks during runtime, as follows: + +* `@NonNullApi`: Used on the package level to declare that the default behavior for parameters and return values is, respectively, neither to accept nor to produce `null` values. +* `@NonNull`: Used on a parameter or return value that must not be `null` (not needed on a parameter and return value where `@NonNullApi` applies). +* `@Nullable`: Used on a parameter or return value that can be `null`. + +Spring annotations are meta-annotated with [JSR 305](https://jcp.org/en/jsr/detail?id=305) annotations (a dormant but widely used JSR). +JSR 305 meta-annotations let tooling vendors (such as [IDEA](https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html), [Eclipse](https://help.eclipse.org/latest/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-using_external_null_annotations.htm), and [Kotlin](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types)) provide null-safety support in a generic way, without having to hard-code support for Spring annotations. +To enable runtime checking of nullability constraints for query methods, you need to activate non-nullability on the package level by using Spring's `@NonNullApi` in `package-info.java`, as shown in the following example: + +*Declaring Non-nullability in `package-info.java`* + +```java +@org.springframework.lang.NonNullApi +package com.acme; +``` + +Once non-null defaulting is in place, repository query method invocations get validated at runtime for nullability constraints. +If a query result violates the defined constraint, an exception is thrown. +This happens when the method would return `null` but is declared as non-nullable (the default with the annotation defined on the package in which the repository resides). +If you want to opt-in to nullable results again, selectively use `@Nullable` on individual methods. +Using the result wrapper types mentioned at the start of this section continues to work as expected: an empty result is translated into the value that represents absence. + +The following example shows a number of the techniques just described: + +*Using different nullability constraints* + +```java +package com.acme; // (1) + +import org.springframework.lang.Nullable; + +interface UserRepository extends Repository { + + User getByEmailAddress(EmailAddress emailAddress); // (2) + + @Nullable + User findByEmailAddress(@Nullable EmailAddress emailAdress); // (3) + + Optional findOptionalByEmailAddress(EmailAddress emailAddress); // (4) +} +``` +```text +1. The repository resides in a package (or sub-package) for which we have defined non-null behavior. +2. Throws an `EmptyResultDataAccessException` when the query does not produce a result. +Throws an `IllegalArgumentException` when the `emailAddress` handed to the method is `null`. +3. Returns `null` when the query does not produce a result. +Also accepts `null` as the value for `emailAddress`. +4. Returns `Optional.empty()` when the query does not produce a result. +Throws an `IllegalArgumentException` when the `emailAddress` handed to the method is `null`. +``` + +## Nullability in Kotlin-based Repositories + +Kotlin has the definition of [nullability constraints](https://kotlinlang.org/docs/reference/null-safety.html) baked into the language. +Kotlin code compiles to bytecode, which does not express nullability constraints through method signatures but rather through compiled-in metadata. +Make sure to include the `kotlin-reflect` JAR in your project to enable introspection of Kotlin's nullability constraints. +Spring Data repositories use the language mechanism to define those constraints to apply the same runtime checks, as follows: + +*Using nullability constraints on Kotlin repositories* + +```kotlin +interface UserRepository : Repository { + + fun findByUsername(username: String): User // (1) + + fun findByFirstname(firstname: String?): User? // (2) +} +``` +```text +1. The method defines both the parameter and the result as non-nullable (the Kotlin default). +The Kotlin compiler rejects method invocations that pass `null` to the method. +If the query yields an empty result, an `EmptyResultDataAccessException` is thrown. +2. This method accepts `null` for the `firstname` parameter and returns `null` if the query does not produce a result. +``` diff --git a/docs/src/content/docs/repositories/object-mapping.md b/docs/src/content/docs/repositories/object-mapping.md new file mode 100644 index 000000000..42c791cde --- /dev/null +++ b/docs/src/content/docs/repositories/object-mapping.md @@ -0,0 +1,429 @@ +--- +title: Object Mapping Fundamentals +description: Spring Data object mapping fundamentals including object creation, property population, and Kotlin support +--- + +This section covers the fundamentals of Spring Data object mapping, object creation, field and property access, mutability and immutability. +Note, that this section only applies to Spring Data modules that do not use the object mapping of the underlying data store (like JPA). +Also be sure to consult the store-specific sections for store-specific object mapping, like indexes, customizing column or field names or the like. + +Core responsibility of the Spring Data object mapping is to create instances of domain objects and map the store-native data structures onto those. +This means we need two fundamental steps: + +1. Instance creation by using one of the constructors exposed. +2. Instance population to materialize all exposed properties. + +## Object creation + +Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type. +The resolution algorithm works as follows: + +1. If there is a single static factory method annotated with `@PersistenceCreator` then it is used. +2. If there is a single constructor, it is used. +3. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used. +4. If the type is a Java `Record` the canonical constructor is used. +5. If there's a no-argument constructor, it is used. +Other constructors will be ignored. + +The value resolution assumes constructor/factory method argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.). +This also requires either parameter names information available in the class file or an `@ConstructorProperties` annotation being present on the constructor. + +The value resolution can be customized by using Spring Framework's `@Value` value annotation using a store-specific SpEL expression. +Please consult the section on store specific mappings for further details. + +### Object creation internals + +To avoid the overhead of reflection, Spring Data object creation uses a factory class generated at runtime by default, which will call the domain classes constructor directly. +I.e. for this example type: + +```java +class Person { + Person(String firstname, String lastname) { … } +} +``` + +we will create a factory class semantically equivalent to this one at runtime: + +```java +class PersonObjectInstantiator implements ObjectInstantiator { + + Object newInstance(Object... args) { + return new Person((String) args[0], (String) args[1]); + } +} +``` + +This gives us a roundabout 10% performance boost over reflection. +For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints: + +- it must not be a private class +- it must not be a non-static inner class +- it must not be a CGLib proxy class +- the constructor to be used by Spring Data must not be private + +If any of these criteria match, Spring Data will fall back to entity instantiation via reflection. + +## Property population + +Once an instance of the entity has been created, Spring Data populates all remaining persistent properties of that class. +Unless already populated by the entity's constructor (i.e. consumed through its constructor argument list), the identifier property will be populated first to allow the resolution of cyclic object references. +After that, all non-transient properties that have not already been populated by the constructor are set on the entity instance. +For that we use the following algorithm: + +1. If the property is immutable but exposes a `with…` method (see below), we use the `with…` method to create a new entity instance with the new property value. +2. If property access (i.e. access through getters and setters) is defined, we're invoking the setter method. +3. If the property is mutable we set the field directly. +4. If the property is immutable we're using the constructor to be used by persistence operations (see [Object creation](#object-creation)) to create a copy of the instance. +5. By default, we set the field value directly. + +### Property population internals + +Similarly to our optimizations in object construction we also use Spring Data runtime generated accessor classes to interact with the entity instance. + +```java +class Person { + + private final Long id; + private String firstname; + private @AccessType(Type.PROPERTY) String lastname; + + Person() { + this.id = null; + } + + Person(Long id, String firstname, String lastname) { + // Field assignments + } + + Person withId(Long id) { + return new Person(id, this.firstname, this.lastname); + } + + void setLastname(String lastname) { + this.lastname = lastname; + } +} +``` + +*A generated Property Accessor* + +```java +class PersonPropertyAccessor implements PersistentPropertyAccessor { + + private static final MethodHandle firstname; // (2) + + private Person person; // (1) + + public void setProperty(PersistentProperty property, Object value) { + + String name = property.getName(); + + if ("firstname".equals(name)) { + firstname.invoke(person, (String) value); // (2) + } else if ("id".equals(name)) { + this.person = person.withId((Long) value); // (3) + } else if ("lastname".equals(name)) { + this.person.setLastname((String) value); // (4) + } + } +} +``` + +1. PropertyAccessor's hold a mutable instance of the underlying object. This is, to enable mutations of otherwise immutable properties. +2. By default, Spring Data uses field-access to read and write property values. As per visibility rules of `private` fields, `MethodHandles` are used to interact with fields. +3. The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated. Calling `withId(…)` creates a new `Person` object. All subsequent mutations will take place in the new instance leaving the previous untouched. +4. Using property-access allows direct method invocations without using `MethodHandles`. + +This gives us a roundabout 25% performance boost over reflection. +For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints: + +* Types must not reside in the default or under the `java` package. +* Types and their constructors must be `public` +* Types that are inner classes must be `static`. +* The used Java Runtime must allow for declaring classes in the originating `ClassLoader`. Java 9 and newer impose certain limitations. + +By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected. + +Let's have a look at the following entity: + +*A Sample entity* + +```java +class Person { + + private final @Id Long id; // (1) + private final String firstname, lastname; // (2) + private final LocalDate birthday; + private final int age; // (3) + + private String comment; // (4) + private @AccessType(Type.PROPERTY) String remarks; // (5) + + static Person of(String firstname, String lastname, LocalDate birthday) { // (6) + + return new Person(null, firstname, lastname, birthday, + Period.between(birthday, LocalDate.now()).getYears()); + } + + Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { // (6) + + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.birthday = birthday; + this.age = age; + } + + Person withId(Long id) { // (1) + return new Person(id, this.firstname, this.lastname, this.birthday, this.age); + } + + void setRemarks(String remarks) { // (5) + this.remarks = remarks; + } +} +``` + +1. The identifier property is final but set to `null` in the constructor. +The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated. +The original `Person` instance stays unchanged as a new one is created. +The same pattern is usually applied for other properties that are store managed but might have to be changed for persistence operations. +The wither method is optional as the persistence constructor (see 6) is effectively a copy constructor and setting the property will be translated into creating a fresh instance with the new identifier value applied. +2. The `firstname` and `lastname` properties are ordinary immutable properties potentially exposed through getters. +3. The `age` property is an immutable but derived one from the `birthday` property. +With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor. +Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no `with…` method being present. +4. The `comment` property is mutable and is populated by setting its field directly. +5. The `remarks` property is mutable and is populated by invoking the setter method. +6. The class exposes a factory method and a constructor for object creation. +The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceCreator`. +Instead, defaulting of properties is handled within the factory method. +If you want Spring Data to use the factory method for object instantiation, annotate it with `@PersistenceCreator`. + +## General recommendations + +* **Try to stick to immutable objects** -- Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only. +Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state. +If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types. +Constructor-only materialization is up to 30% faster than properties population. +* **Provide an all-args constructor** -- Even if you cannot or don't want to model your entities as immutable values, there's still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones, as this allows the object mapping to skip the property population for optimal performance. +* **Use factory methods instead of overloaded constructors to avoid `@PersistenceCreator`** -- With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors that omit things like auto-generated identifiers etc. +It's an established pattern to rather use static factory methods to expose these variants of the all-args constructor. +* **Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used** -- +* **For identifiers to be generated, still use a final field in combination with an all-arguments persistence constructor (preferred) or a `with…` method** -- +* **Use Lombok to avoid boilerplate code** -- As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`. + +### Overriding Properties + +Java allows a flexible design of domain classes where a subclass could define a property that is already declared with the same name in its superclass. +Consider the following example: + +```java +public class SuperType { + + private CharSequence field; + + public SuperType(CharSequence field) { + this.field = field; + } + + public CharSequence getField() { + return this.field; + } + + public void setField(CharSequence field) { + this.field = field; + } +} + +public class SubType extends SuperType { + + private String field; + + public SubType(String field) { + super(field); + this.field = field; + } + + @Override + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + + // optional + super.setField(field); + } +} +``` + +Both classes define a `field` using assignable types. `SubType` however shadows `SuperType.field`. +Depending on the class design, using the constructor could be the only default approach to set `SuperType.field`. +Alternatively, calling `super.setField(…)` in the setter could set the `field` in `SuperType`. +All these mechanisms create conflicts to some degree because the properties share the same name yet might represent two distinct values. +Spring Data skips super-type properties if types are not assignable. +That is, the type of the overridden property must be assignable to its super-type property type to be registered as override, otherwise the super-type property is considered transient. +We generally recommend using distinct property names. + +Spring Data modules generally support overridden properties holding different values. +From a programming model perspective there are a few things to consider: + +1. Which property should be persisted (default to all declared properties)? +You can exclude properties by annotating these with `@Transient`. +2. How to represent properties in your data store? +Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name. +3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be generally set without making any further assumptions of the setter implementation. + +## Kotlin support + +Spring Data adapts specifics of Kotlin to allow object creation and mutation. + +### Kotlin object creation + +Kotlin classes are supported to be instantiated, all classes are immutable by default and require explicit property declarations to define mutable properties. + +Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type. +The resolution algorithm works as follows: + +1. If there is a constructor that is annotated with `@PersistenceCreator`, it is used. +2. If the type is a Kotlin data class the primary constructor is used. +3. If there is a single static factory method annotated with `@PersistenceCreator` then it is used. +4. If there is a single constructor, it is used. +5. If there are multiple constructors and exactly one is annotated with `@PersistenceCreator`, it is used. +6. If the type is a Java `Record` the canonical constructor is used. +7. If there's a no-argument constructor, it is used. +Other constructors will be ignored. + +Consider the following `data` class `Person`: + +```kotlin +data class Person(val id: String, val name: String) +``` + +The class above compiles to a typical class with an explicit constructor. We can customize this class by adding another constructor and annotate it with `@PersistenceCreator` to indicate a constructor preference: + +```kotlin +data class Person(var id: String, val name: String) { + + @PersistenceCreator + constructor(id: String) : this(id, "unknown") +} +``` + +Kotlin supports parameter optionality by allowing default values to be used if a parameter is not provided. +When Spring Data detects a constructor with parameter defaulting, then it leaves these parameters absent if the data store does not provide a value (or simply returns `null`) so Kotlin can apply parameter defaulting. Consider the following class that applies parameter defaulting for `name` + +```kotlin +data class Person(var id: String, val name: String = "unknown") +``` + +Every time the `name` parameter is either not part of the result or its value is `null`, then the `name` defaults to `unknown`. + +:::note +Delegated properties are not supported with Spring Data. The mapping metadata filters delegated properties for Kotlin Data classes. +In all other cases you can exclude synthetic fields for delegated properties by annotating the property with `@Transient`. +::: + +### Property population of Kotlin data classes + +In Kotlin, all classes are immutable by default and require explicit property declarations to define mutable properties. +Consider the following `data` class `Person`: + +```kotlin +data class Person(val id: String, val name: String) +``` + +This class is effectively immutable. +It allows creating new instances as Kotlin generates a `copy(…)` method that creates new object instances copying all property values from the existing object and applying property values provided as arguments to the method. + +### Kotlin Overriding Properties + +Kotlin allows declaring [property overrides](https://kotlinlang.org/docs/inheritance.html#overriding-properties) to alter properties in subclasses. + +```kotlin +open class SuperType(open var field: Int) + +class SubType(override var field: Int = 1) : + SuperType(field) { +} +``` + +Such an arrangement renders two properties with the name `field`. +Kotlin generates property accessors (getters and setters) for each property in each class. +Effectively, the code looks like as follows: + +```java +public class SuperType { + + private int field; + + public SuperType(int field) { + this.field = field; + } + + public int getField() { + return this.field; + } + + public void setField(int field) { + this.field = field; + } +} + +public final class SubType extends SuperType { + + private int field; + + public SubType(int field) { + super(field); + this.field = field; + } + + public int getField() { + return this.field; + } + + public void setField(int field) { + this.field = field; + } +} +``` + +Getters and setters on `SubType` set only `SubType.field` and not `SuperType.field`. +In such an arrangement, using the constructor is the only default approach to set `SuperType.field`. +Adding a method to `SubType` to set `SuperType.field` via `this.SuperType.field = …` is possible but falls outside of supported conventions. +Property overrides create conflicts to some degree because the properties share the same name yet might represent two distinct values. +We generally recommend using distinct property names. + +Spring Data modules generally support overridden properties holding different values. +From a programming model perspective there are a few things to consider: + +1. Which property should be persisted (default to all declared properties)? +You can exclude properties by annotating these with `@Transient`. +2. How to represent properties in your data store? +Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name. +3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be set. + +### Kotlin Value Classes + +Kotlin Value Classes are designed for a more expressive domain model to make underlying concepts explicit. +Spring Data can read and write types that define properties using Value Classes. + +Consider the following domain model: + +```kotlin +@JvmInline +value class EmailAddress(val theAddress: String) // (1) + +data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) // (2) +``` + +1. A simple value class with a non-nullable value type. +2. Data class defining a property using the `EmailAddress` value class. + +:::note +Non-nullable properties using non-primitive value types are flattened in the compiled class to the value type. +Nullable primitive value types or nullable value-in-value types are represented with their wrapper type and that affects how value types are represented in the database. +::: diff --git a/docs/src/content/docs/repositories/projections.md b/docs/src/content/docs/repositories/projections.md new file mode 100644 index 000000000..17b517d12 --- /dev/null +++ b/docs/src/content/docs/repositories/projections.md @@ -0,0 +1,290 @@ +--- +title: Projections +description: Spring Data projections including interface-based, class-based, and dynamic projections +--- + +Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository. +However, it might sometimes be desirable to create projections based on certain attributes of those types. +Spring Data allows modeling dedicated return types, to more selectively retrieve partial views of the managed aggregates. + +Imagine a repository and aggregate root type such as the following example: + +*A sample aggregate and repository* + +```java +class Person { + + @Id UUID id; + String firstname, lastname; + Address address; + + static class Address { + String zipCode, city, street; + } +} + +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname); +} +``` + +Now imagine that we want to retrieve the person's name attributes only. +What means does Spring Data offer to achieve this? +The rest of this chapter answers that question. + +:::note +Projection types are types residing outside the entity's type hierarchy. +Superclasses and interfaces implemented by the entity are inside the type hierarchy hence returning a supertype (or implemented interface) returns an instance of the fully materialized entity. +::: + +## Interface-based Projections + +The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example: + +*A projection interface to retrieve a subset of attributes* + +```java +interface NamesOnly { + + String getFirstname(); + String getLastname(); +} +``` + +The important bit here is that the properties defined here exactly match properties in the aggregate root. +Doing so lets a query method be added as follows: + +A repository using an interface based projection with a query method: + +*A repository using an interface based projection with a query method* + +```java +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname); +} +``` + +The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object. + +:::note +Declaring a method in your `Repository` that overrides a base method (e.g. declared in `CrudRepository`, a store-specific repository interface, or the `Simple…Repository`) results in a call to the base method regardless of the declared return type. +Make sure to use a compatible return type as base methods cannot be used for projections. +Some store modules support `@Query` annotations to turn an overridden base method into a query method that then can be used to return projections. +::: + +Projections can be used recursively. +If you want to include some of the `Address` information as well, create a projection interface for that and return that interface from the declaration of `getAddress()`, as shown in the following example: + +*A projection interface to retrieve a subset of attributes* + +```java +interface PersonSummary { + + String getFirstname(); + String getLastname(); + AddressSummary getAddress(); + + interface AddressSummary { + String getCity(); + } +} +``` + +On method invocation, the `address` property of the target instance is obtained and wrapped into a projecting proxy in turn. + +### Closed Projections + +A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection. +The following example (which we used earlier in this chapter, too) is a closed projection: + +*A closed projection* + +```java +interface NamesOnly { + + String getFirstname(); + String getLastname(); +} +``` + +If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy. +For more details on that, see the module-specific part of the reference documentation. + +### Open Projections + +Accessor methods in projection interfaces can also be used to compute new values by using the `@Value` annotation, as shown in the following example: + +*An Open Projection* + +```java +interface NamesOnly { + + @Value("#{target.firstname + ' ' + target.lastname}") + String getFullName(); + … +} +``` + +The aggregate root backing the projection is available in the `target` variable. +A projection interface using `@Value` is an open projection. +Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root. + +The expressions used in `@Value` should not be too complex -- you want to avoid programming in `String` variables. +For very simple expressions, one option might be to resort to default methods (introduced in Java 8), as shown in the following example: + +*A projection interface using a default method for custom logic* + +```java +interface NamesOnly { + + String getFirstname(); + String getLastname(); + + default String getFullName() { + return getFirstname().concat(" ").concat(getLastname()); + } +} +``` + +This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface. +A second, more flexible, option is to implement the custom logic in a Spring bean and then invoke that from the SpEL expression, as shown in the following example: + +*Sample Person object* + +```java +@Component +class MyBean { + + String getFullName(Person person) { + … + } +} + +interface NamesOnly { + + @Value("#{@myBean.getFullName(target)}") + String getFullName(); + … +} +``` + +Notice how the SpEL expression refers to `myBean` and invokes the `getFullName(…)` method and forwards the projection target as a method parameter. +Methods backed by SpEL expression evaluation can also use method parameters, which can then be referred to from the expression. +The method parameters are available through an `Object` array named `args`. +The following example shows how to get a method parameter from the `args` array: + +*Sample Person object* + +```java +interface NamesOnly { + + @Value("#{args[0] + ' ' + target.firstname + '!'}") + String getSalutation(String prefix); +} +``` + +Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described earlier. + +### Nullable Wrappers + +Getters in projection interfaces can make use of nullable wrappers for improved null-safety. +Currently supported wrapper types are: + +* `java.util.Optional` +* `com.google.common.base.Optional` +* `scala.Option` +* `io.vavr.control.Option` + +*A projection interface using nullable wrappers* + +```java +interface NamesOnly { + + Optional getFirstname(); +} +``` + +If the underlying projection value is not `null`, then values are returned using the present-representation of the wrapper type. +In case the backing value is `null`, then the getter method returns the empty representation of the used wrapper type. + +## Class-based Projections (DTOs) + +Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved. +These DTO types can be used in exactly the same way projection interfaces are used, except that no proxying happens and no nested projections can be applied. + +If the store optimizes the query execution by limiting the fields to be loaded, the fields to be loaded are determined from the parameter names of the constructor that is exposed. + +The following example shows a projecting DTO: + +*A projecting DTO* + +```java +record NamesOnly(String firstname, String lastname) { +} +``` + +Java Records are ideal to define DTO types since they adhere to value semantics: +All fields are `private final` and `equals(…)`/`hashCode()`/`toString()` methods are created automatically. +Alternatively, you can use any class that defines the properties you want to project. + +## Dynamic Projections + +So far, we have used the projection type as the return type or element type of a collection. +However, you might want to select the type to be used at invocation time (which makes it dynamic). +To apply dynamic projections, use a query method such as the one shown in the following example: + +*A repository using a dynamic projection parameter* + +```java +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname, Class type); +} +``` + +This way, the method can be used to obtain the aggregates as is or with a projection applied, as shown in the following example: + +*Using a repository with dynamic projections* + +```java +void someMethod(PersonRepository people) { + + Collection aggregates = + people.findByLastname("Matthews", Person.class); + + Collection aggregates = + people.findByLastname("Matthews", NamesOnly.class); +} +``` + +:::note +Query parameters of type `Class` are inspected whether they qualify as dynamic projection parameter. +If the actual return type of the query equals the generic parameter type of the `Class` parameter, then the matching `Class` parameter is not available for usage within the query or SpEL expressions. +If you want to use a `Class` parameter as query argument then make sure to use a different generic parameter, for example `Class`. +::: + +:::note +When using [Class-based projection](#class-based-projections-dtos), types must declare a single constructor so that Spring Data can determine their input properties. +If your class defines more than one constructor, then you cannot use the type without further hints for DTO projections. +In such a case annotate the desired constructor with `@PersistenceCreator` as outlined below so that Spring Data can determine which properties to select: + +```java +public class NamesOnly { + + private final String firstname; + private final String lastname; + + protected NamesOnly() { } + + @PersistenceCreator + public NamesOnly(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + // ... +} +``` +::: diff --git a/docs/src/content/docs/repositories/query-keywords-reference.md b/docs/src/content/docs/repositories/query-keywords-reference.md new file mode 100644 index 000000000..cb5951e82 --- /dev/null +++ b/docs/src/content/docs/repositories/query-keywords-reference.md @@ -0,0 +1,84 @@ +--- +title: Repository query keywords +description: Reference guide for supported query method keywords, reserved methods, and predicate modifiers +--- + +## Supported query method subject keywords + +The following table lists the subject keywords generally supported by the Spring Data repository query derivation mechanism to express the predicate. +Consult the store-specific documentation for the exact list of supported keywords, because some keywords listed here might not be supported in a particular store. + +*Table 1. Query subject keywords* + +| Keyword | Description | +|---------|-------------| +| `find…By`, `read…By`, `get…By`, `query…By`, `search…By`, `stream…By` | General query method returning typically the repository type, a `Collection` or `Streamable` subtype or a result wrapper such as `Page`, `GeoResults` or any other store-specific result wrapper. Can be used as `findBy…`, `findMyDomainTypeBy…` or in combination with additional keywords. | +| `exists…By` | Exists projection, returning typically a `boolean` result. | +| `count…By` | Count projection returning a numeric result. | +| `delete…By`, `remove…By` | Delete query method returning either no result (`void`) or the delete count. | +| `…First…`, `…Top…` | Limit the query results to the first `` of results. This keyword can occur in any place of the subject between `find` (and the other keywords) and `by`. | +| `…Distinct…` | Use a distinct query to return only unique results. Consult the store-specific documentation whether that feature is supported. This keyword can occur in any place of the subject between `find` (and the other keywords) and `by`. | + +## Reserved methods + +The following table lists reserved methods that use predefined functionality (as defined in `CrudRepository`). +These methods are directly invoked on the backing (store-specific) implementation of the repository proxy. +See also "[Defining Query Methods](/repositories/query-methods-details#reserved-method-names)". + +*Table 2. Reserved methods* + +| | +|---------| +| `deleteAllById(Iterable identifiers)` | +| `deleteById(ID identifier)` | +| `existsById(ID identifier)` | +| `findAllById(Iterable identifiers)` | +| `findById(ID identifier)` | + +## Supported query method predicate keywords and modifiers + +The following table lists the predicate keywords generally supported by the Spring Data repository query derivation mechanism. +However, consult the store-specific documentation for the exact list of supported keywords, because some keywords listed here might not be supported in a particular store. + +*Table 3. Query predicate keywords* + +| Logical keyword | Keyword expressions | +|-----------------|---------------------| +| `AND` | `And` | +| `OR` | `Or` | +| `AFTER` | `After`, `IsAfter` | +| `BEFORE` | `Before`, `IsBefore` | +| `CONTAINING` | `Containing`, `IsContaining`, `Contains` | +| `BETWEEN` | `Between`, `IsBetween` | +| `ENDING_WITH` | `EndingWith`, `IsEndingWith`, `EndsWith` | +| `EXISTS` | `Exists` | +| `FALSE` | `False`, `IsFalse` | +| `GREATER_THAN` | `GreaterThan`, `IsGreaterThan` | +| `GREATER_THAN_EQUALS` | `GreaterThanEqual`, `IsGreaterThanEqual` | +| `IN` | `In`, `IsIn` | +| `IS` | `Is`, `Equals`, (or no keyword) | +| `IS_EMPTY` | `IsEmpty`, `Empty` | +| `IS_NOT_EMPTY` | `IsNotEmpty`, `NotEmpty` | +| `IS_NOT_NULL` | `NotNull`, `IsNotNull` | +| `IS_NULL` | `Null`, `IsNull` | +| `LESS_THAN` | `LessThan`, `IsLessThan` | +| `LESS_THAN_EQUAL` | `LessThanEqual`, `IsLessThanEqual` | +| `LIKE` | `Like`, `IsLike` | +| `NEAR` | `Near`, `IsNear` | +| `NOT` | `Not`, `IsNot` | +| `NOT_IN` | `NotIn`, `IsNotIn` | +| `NOT_LIKE` | `NotLike`, `IsNotLike` | +| `REGEX` | `Regex`, `MatchesRegex`, `Matches` | +| `STARTING_WITH` | `StartingWith`, `IsStartingWith`, `StartsWith` | +| `TRUE` | `True`, `IsTrue` | +| `WITHIN` | `Within`, `IsWithin` | + +In addition to filter predicates, the following list of modifiers is supported: + +*Table 4. Query predicate modifier keywords* + +| Keyword | Description | +|---------|-------------| +| `IgnoreCase`, `IgnoringCase` | Used with a predicate keyword for case-insensitive comparison. | +| `AllIgnoreCase`, `AllIgnoringCase` | Ignore case for all suitable properties. Used somewhere in the query method predicate. | +| `OrderBy…` | Specify a static sorting order followed by the property path and direction (e. g. `OrderByFirstnameAscLastnameDesc`). | diff --git a/docs/src/content/docs/repositories/query-methods-details.md b/docs/src/content/docs/repositories/query-methods-details.md new file mode 100644 index 000000000..e69eefdc3 --- /dev/null +++ b/docs/src/content/docs/repositories/query-methods-details.md @@ -0,0 +1,459 @@ +--- +title: Defining Query Methods +description: Comprehensive guide to defining query methods including query creation, property expressions, streaming, and paging +--- + +The repository proxy has two ways to derive a store-specific query from the method name: + +* By deriving the query from the method name directly. +* By using a manually defined query. + +Available options depend on the actual store. +However, there must be a strategy that decides what actual query is created. +The next section describes the available options. + +## Query Lookup Strategies + +The following strategies are available for the repository infrastructure to resolve the query. +For Java configuration, you can use the `queryLookupStrategy` attribute of the `Enable{store}Repositories` annotation. +Some strategies may not be supported for particular datastores. + +- `CREATE` attempts to construct a store-specific query from the query method name. +The general approach is to remove a given set of well known prefixes from the method name and parse the rest of the method. +You can read more about query construction in "[Query Creation](#query-creation)". + +- `USE_DECLARED_QUERY` tries to find a declared query and throws an exception if it cannot find one. +The query can be defined by an annotation somewhere or declared by other means. +See the documentation of the specific store to find available options for that store. +If the repository infrastructure does not find a declared query for the method at bootstrap time, it fails. + +- `CREATE_IF_NOT_FOUND` (the default) combines `CREATE` and `USE_DECLARED_QUERY`. +It looks up a declared query first, and, if no declared query is found, it creates a custom method name-based query. +This is the default lookup strategy and, thus, is used if you do not configure anything explicitly. +It allows quick query definition by method names but also custom-tuning of these queries by introducing declared queries as needed. + +## Query Creation + +The query builder mechanism built into the Spring Data repository infrastructure is useful for building constraining queries over entities of the repository. + +The following example shows how to create a number of queries: + +*Query creation from method names* +```java +interface PersonRepository extends Repository { + + List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); + + // Enables the distinct flag for the query + List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); + + // Enabling ignoring case for an individual property + List findByLastnameIgnoreCase(String lastname); + // Enabling ignoring case for all suitable properties + List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); + + // Enabling static ORDER BY for a query + List findByLastnameOrderByFirstnameAsc(String lastname); + List findByLastnameOrderByFirstnameDesc(String lastname); +} +``` + +Parsing query method names is divided into subject and predicate. +The first part (`find…By`, `exists…By`) defines the subject of the query, the second part forms the predicate. +The introducing clause (subject) can contain further expressions. +Any text between `find` (or other introducing keywords) and `By` is considered to be descriptive unless using one of the result-limiting keywords such as a `Distinct` to set a distinct flag on the query to be created or `Top`/`First` to limit query results. + +The appendix contains the [full list of query method subject keywords](/repositories/query-keywords-reference#supported-query-method-subject-keywords) and [query method predicate keywords including sorting and letter-casing modifiers](/repositories/query-keywords-reference#supported-query-method-predicate-keywords-and-modifiers). +However, the first `By` acts as a delimiter to indicate the start of the actual criteria predicate. +At a very basic level, you can define conditions on entity properties and concatenate them with `And` and `Or`. + +The actual result of parsing the method depends on the persistence store for which you create the query. +However, there are some general things to notice: + +- The expressions are usually property traversals combined with operators that can be concatenated. +You can combine property expressions with `AND` and `OR`. +You also get support for operators such as `Between`, `LessThan`, `GreaterThan`, and `Like` for the property expressions. +The supported operators can vary by datastore, so consult the appropriate part of your reference documentation. + +- The method parser supports setting an `IgnoreCase` flag for individual properties (for example, `findByLastnameIgnoreCase(…)`) or for all properties of a type that supports ignoring case (usually `String` instances -- for example, `findByLastnameAndFirstnameAllIgnoreCase(…)`). +Whether ignoring cases is supported may vary by store, so consult the relevant sections in the reference documentation for the store-specific query method. + +- You can apply static ordering by appending an `OrderBy` clause to the query method that references a property and by providing a sorting direction (`Asc` or `Desc`). +To create a query method that supports dynamic sorting, see "[Paging, Iterating Large Results, Sorting & Limiting](#paging-iterating-large-results-sorting--limiting)". + +## Reserved Method Names + +While derived repository methods bind to properties by name, there are a few exceptions to this rule when it comes to certain method names inherited from the base repository targeting the _identifier_ property. +Those _reserved methods_ like `CrudRepository#findById` (or just `findById`) are targeting the _identifier_ property regardless of the actual property name used in the declared method. + +Consider the following domain type holding a property `pk` marked as the identifier via `@Id` and a property called `id`. +In this case you need to pay close attention to the naming of your lookup methods as they may collide with predefined signatures: + +```java +class User { + @Id Long pk; // <1> + + Long id; // <2> + + // … +} + +interface UserRepository extends Repository { + + Optional findById(Long id); // <3> + + Optional findByPk(Long pk); // <4> + + Optional findUserById(Long id); // <5> +} +``` +```text +1. The identifier property (primary key). +2. A property named `id`, but not the identifier. +3. Targets the `pk` property (the one marked with `@Id` which is considered to be the identifier) as it refers to a `CrudRepository` base repository method. +Therefore, it is not a derived query using of `id` as the property name would suggest because it is one of the _reserved methods_. +4. Targets the `pk` property by name as it is a derived query. +5. Targets the `id` property by using the descriptive token between `find` and `by` to avoid collisions with _reserved methods_. +``` + +This special behaviour not only targets lookup methods but also applies to the `exits` and `delete` ones. +Please refer to the "[Repository query keywords](/repositories/query-keywords-reference#reserved-methods)" for the list of methods. + +## Property Expressions + +Property expressions can refer only to a direct property of the managed entity, as shown in the preceding example. +At query creation time, you already make sure that the parsed property is a property of the managed domain class. +However, you can also define constraints by traversing nested properties. +Consider the following method signature: + +```java +List findByAddressZipCode(ZipCode zipCode); +``` + +Assume a `Person` has an `Address` with a `ZipCode`. +In that case, the method creates the `x.address.zipCode` property traversal. +The resolution algorithm starts by interpreting the entire part (`AddressZipCode`) as the property and checks the domain class for a property with that name (uncapitalized). +If the algorithm succeeds, it uses that property. +If not, the algorithm splits up the source at the camel-case parts from the right side into a head and a tail and tries to find the corresponding property -- in our example, `AddressZip` and `Code`. +If the algorithm finds a property with that head, it takes the tail and continues building the tree down from there, splitting the tail up in the way just described. +If the first split does not match, the algorithm moves the split point to the left (`Address`, `ZipCode`) and continues. + +Although this should work for most cases, it is possible for the algorithm to select the wrong property. +Suppose the `Person` class has an `addressZip` property as well. +The algorithm would match in the first split round already, choose the wrong property, and fail (as the type of `addressZip` probably has no `code` property). + +To resolve this ambiguity you can use `_` inside your method name to manually define traversal points. +So our method name would be as follows: + +```java +List findByAddress_ZipCode(ZipCode zipCode); +``` + +:::note +Because we treat underscores (`_`) as a reserved character, we strongly advise to follow standard Java naming conventions (that is, not using underscores in property names but applying camel case instead). +::: + +:::caution +*Field Names starting with underscore:* +Field names may start with underscores like `String _name`. +Make sure to preserve the `_` as in `_name` and use double `_` to split nested paths like `user__name`. + +*Upper Case Field Names:* +Field names that are all uppercase can be used as such. +Nested paths if applicable require splitting via `_` as in `USER_name`. + +*Field Names with 2nd uppercase letter:* +Field names that consist of a starting lower case letter followed by an uppercase one like `String qCode` can be resolved by starting with two upper case letters as in `QCode`. +Please be aware of potential path ambiguities. + +*Path Ambiguities:* +In the following sample the arrangement of properties `qCode` and `q`, with `q` containing a property called `code`, creates an ambiguity for the path `QCode`. +```java +record Container(String qCode, Code q) {} +record Code(String code) {} +``` +Since a direct match on a property is considered first, any potential nested paths will not be considered and the algorithm picks the `qCode` field. +In order to select the `code` field in `q` the underscore notation `Q_Code` is required. +::: + +## Repository Methods Returning Collections or Iterables + +Query methods that return multiple results can use standard Java `Iterable`, `List`, and `Set`. +Beyond that, we support returning Spring Data's `Streamable`, a custom extension of `Iterable`, as well as collection types provided by [Vavr](https://www.vavr.io/). +Refer to the appendix explaining all possible [query method return types](/repositories/query-return-types-reference). + +### Using Streamable as Query Method Return Type + +You can use `Streamable` as alternative to `Iterable` or any collection type. +It provides convenience methods to access a non-parallel `Stream` (missing from `Iterable`) and the ability to directly `….filter(…)` and `….map(…)` over the elements and concatenate the `Streamable` to others: + +*Using Streamable to combine query method results* +```java +interface PersonRepository extends Repository { + Streamable findByFirstnameContaining(String firstname); + Streamable findByLastnameContaining(String lastname); +} + +Streamable result = repository.findByFirstnameContaining("av") + .and(repository.findByLastnameContaining("ea")); +``` + +### Returning Custom Streamable Wrapper Types + +Providing dedicated wrapper types for collections is a commonly used pattern to provide an API for a query result that returns multiple elements. +Usually, these types are used by invoking a repository method returning a collection-like type and creating an instance of the wrapper type manually. +You can avoid that additional step as Spring Data lets you use these wrapper types as query method return types if they meet the following criteria: + +1. The type implements `Streamable`. +2. The type exposes either a constructor or a static factory method named `of(…)` or `valueOf(…)` that takes `Streamable` as an argument. + +The following listing shows an example: + +```java +class Product { // <1> + MonetaryAmount getPrice() { … } +} + +@RequiredArgsConstructor(staticName = "of") +class Products implements Streamable { // <2> + + private final Streamable streamable; + + public MonetaryAmount getTotal() { // <3> + return streamable.stream() + .map(Product::getPrice) + .reduce(Money.of(0), MonetaryAmount::add); + } + + + @Override + public Iterator iterator() { // <4> + return streamable.iterator(); + } +} + +interface ProductRepository implements Repository { + Products findAllByDescriptionContaining(String text); // <5> +} +``` +```text +1. A `Product` entity that exposes API to access the product's price. +2. A wrapper type for a `Streamable` that can be constructed by using `Products.of(…)` (factory method created with the Lombok annotation). + A standard constructor taking the `Streamable` will do as well. +3. The wrapper type exposes an additional API, calculating new values on the `Streamable`. +4. Implement the `Streamable` interface and delegate to the actual result. +5. That wrapper type `Products` can be used directly as a query method return type. +You do not need to return `Streamable` and manually wrap it after the query in the repository client. +``` + +### Support for Vavr Collections + +[Vavr](https://www.vavr.io/) is a library that embraces functional programming concepts in Java. +It ships with a custom set of collection types that you can use as query method return types, as the following table shows: + +| Vavr collection type | Used Vavr implementation type | Valid Java source types | +|---------------------|-------------------------------|-------------------------| +| `io.vavr.collection.Seq` | `io.vavr.collection.List` | `java.util.Iterable` | +| `io.vavr.collection.Set` | `io.vavr.collection.LinkedHashSet` | `java.util.Iterable` | +| `io.vavr.collection.Map` | `io.vavr.collection.LinkedHashMap` | `java.util.Map` | + +You can use the types in the first column (or subtypes thereof) as query method return types and get the types in the second column used as implementation type, depending on the Java type of the actual query result (third column). +Alternatively, you can declare `Traversable` (the Vavr `Iterable` equivalent), and we then derive the implementation class from the actual return value. +That is, a `java.util.List` is turned into a Vavr `List` or `Seq`, a `java.util.Set` becomes a Vavr `LinkedHashSet` `Set`, and so on. + +## Streaming Query Results + +You can process the results of query methods incrementally by using a Java 8 `Stream` as the return type. +Instead of wrapping the query results in a `Stream`, data store-specific methods are used to perform the streaming, as shown in the following example: + +*Stream the result of a query with Java 8 `Stream`* + +```java +@Query("select u from User u") +Stream findAllByCustomQueryAndStream(); + +Stream readAllByFirstnameNotNull(); + +@Query("select u from User u") +Stream streamAllPaged(Pageable pageable); +``` + +:::note +A `Stream` potentially wraps underlying data store-specific resources and must, therefore, be closed after usage. +You can either manually close the `Stream` by using the `close()` method or by using a Java 7 `try-with-resources` block, as shown in the following example: +::: + +*Working with a `Stream` result in a `try-with-resources` block* + +```java +try (Stream stream = repository.findAllByCustomQueryAndStream()) { + stream.forEach(…); +} +``` + +:::note +Not all Spring Data modules currently support `Stream` as a return type. +::: + +## Asynchronous Query Results + +You can run repository queries asynchronously by using [Spring's asynchronous method running capability](https://docs.spring.io/spring-framework/reference/integration/scheduling.html). +This means the method returns immediately upon invocation while the actual query occurs in a task that has been submitted to a Spring `TaskExecutor`. +Asynchronous queries differ from reactive queries and should not be mixed. +See the store-specific documentation for more details on reactive support. +The following example shows a number of asynchronous queries: + +```java +@Async +Future findByFirstname(String firstname); // <1> + +@Async +CompletableFuture findOneByFirstname(String firstname); // <2> +``` +```text +1. Use `java.util.concurrent.Future` as the return type. +2. Use a Java 8 `java.util.concurrent.CompletableFuture` as the return type. +``` + +## Paging, Iterating Large Results, Sorting & Limiting + +To handle parameters in your query, define method parameters as already seen in the preceding examples. +Besides that, the infrastructure recognizes certain specific types like `Pageable`, `Sort` and `Limit`, to apply pagination, sorting and limiting to your queries dynamically. +The following example demonstrates these features: + +*Using `Pageable`, `Slice`, `Sort` and `Limit` in query methods* + +```java +Page findByLastname(String lastname, Pageable pageable); + +Slice findByLastname(String lastname, Pageable pageable); + +List findByLastname(String lastname, Sort sort); + +List findByLastname(String lastname, Sort sort, Limit limit); + +List findByLastname(String lastname, Pageable pageable); +``` + +:::note[Important] +APIs taking `Sort`, `Pageable` and `Limit` expect non-`null` values to be handed into methods. +If you do not want to apply any sorting or pagination, use `Sort.unsorted()`, `Pageable.unpaged()` and `Limit.unlimited()`. +::: + +The first method lets you pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query. +A `Page` knows about the total number of elements and pages available. +It does so by the infrastructure triggering a count query to calculate the overall number. +As this might be expensive (depending on the store used), you can instead return a `Slice`. +A `Slice` knows only about whether a next `Slice` is available, which might be sufficient when walking through a larger result set. + +Sorting options are handled through the `Pageable` instance, too. +If you need only sorting, add an `org.springframework.data.domain.Sort` parameter to your method. +As you can see, returning a `List` is also possible. +In this case, the additional metadata required to build the actual `Page` instance is not created (which, in turn, means that the additional count query that would have been necessary is not issued). +Rather, it restricts the query to look up only the given range of entities. + +:::note +To find out how many pages you get for an entire query, you have to trigger an additional count query. +By default, this query is derived from the query you actually trigger. +::: + +:::note[Important] +Special parameters may only be used once within a query method. +Some special parameters described above are mutually exclusive. +Please consider the following list of invalid parameter combinations. + +| Parameters | Example | Reason | +|-----------|---------|--------| +| `Pageable` and `Sort` | `findBy...(Pageable page, Sort sort)` | `Pageable` already defines `Sort` | +| `Pageable` and `Limit` | `findBy...(Pageable page, Limit limit)` | `Pageable` already defines a limit. | + +The `Top` keyword used to limit results can be used to along with `Pageable` whereas `Top` defines the total maximum of results, whereas the Pageable parameter may reduce this number. +::: + +### Which Method is Appropriate? + +The value provided by the Spring Data abstractions is perhaps best shown by the possible query method return types outlined in the following table below. +The table shows which types you can return from a query method + +*Table 1. Consuming Large Query Results* + +| Method | Amount of Data Fetched | Query Structure | Constraints | +|--------|----------------------|-----------------|-------------| +| [`List`](/repositories/query-methods-details#repository-methods-returning-collections-or-iterables) | All results. | Single query. | Query results can exhaust all memory. Fetching all data can be time-intensive. | +| [`Streamable`](/repositories/query-methods-details#using-streamable-as-query-method-return-type) | All results. | Single query. | Query results can exhaust all memory. Fetching all data can be time-intensive. | +| [`Stream`](/repositories/query-methods-details#streaming-query-results) | Chunked (one-by-one or in batches) depending on `Stream` consumption. | Single query using typically cursors. | Streams must be closed after usage to avoid resource leaks. | +| `Flux` | Chunked (one-by-one or in batches) depending on `Flux` consumption. | Single query using typically cursors. | Store module must provide reactive infrastructure. | +| `Slice` | `Pageable.getPageSize() + 1` at `Pageable.getOffset()` | One to many queries fetching data starting at `Pageable.getOffset()` applying limiting. | A `Slice` can only navigate to the next `Slice`. `Slice` provides details whether there is more data to fetch. Offset-based queries becomes inefficient when the offset is too large because the database still has to materialize the full result. | +| `Page` | `Pageable.getPageSize()` at `Pageable.getOffset()` | One to many queries starting at `Pageable.getOffset()` applying limiting. Additionally, `COUNT(…)` query to determine the total number of elements can be required. | Often times, `COUNT(…)` queries are required that are costly. Offset-based queries becomes inefficient when the offset is too large because the database still has to materialize the full result. | + +### Paging and Sorting + +You can define simple sorting expressions by using property names. +You can concatenate expressions to collect multiple criteria into one expression. + +*Defining sort expressions* + +```java +Sort sort = Sort.by("firstname").ascending() + .and(Sort.by("lastname").descending()); +``` + +For a more type-safe way to define sort expressions, start with the type for which to define the sort expression and use method references to define the properties on which to sort. + +*Defining sort expressions by using the type-safe API* + +```java +TypedSort person = Sort.sort(Person.class); + +Sort sort = person.by(Person::getFirstname).ascending() + .and(person.by(Person::getLastname).descending()); +``` + +:::note +`TypedSort.by(…)` makes use of runtime proxies by (typically) using CGlib, which may interfere with native image compilation when using tools such as Graal VM Native. +::: + +If your store implementation supports Querydsl, you can also use the generated metamodel types to define sort expressions: + +*Defining sort expressions by using the Querydsl API* + +```java +QSort sort = QSort.by(QPerson.firstname.asc()) + .and(QSort.by(QPerson.lastname.desc())); +``` + +### Limiting Query Results + +In addition to paging it is possible to limit the result size using a dedicated `Limit` parameter. +You can also limit the results of query methods by using the `First` or `Top` keywords, which you can use interchangeably but may not be mixed with a `Limit` parameter. +You can append an optional numeric value to `Top` or `First` to specify the maximum result size to be returned. +If the number is left out, a result size of 1 is assumed. +The following example shows how to limit the query size: + +*Limiting the result size of a query with `Top` and `First`* + +```java +List findByLastname(String lastname, Limit limit); + +User findFirstByOrderByLastnameAsc(); + +User findTopByLastnameOrderByAgeDesc(String lastname); + +Page queryFirst10ByLastname(String lastname, Pageable pageable); + +Slice findTop3By(Pageable pageable); + +List findFirst10ByLastname(String lastname, Sort sort); + +List findTop10ByLastname(String lastname, Pageable pageable); +``` + +The limiting expressions also support the `Distinct` keyword for datastores that support distinct queries. +Also, for the queries that limit the result set to one instance, wrapping the result into with the `Optional` keyword is supported. + +If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of available pages), it is applied within the limited result. + +:::note +Limiting the results in combination with dynamic sorting by using a `Sort` parameter lets you express query methods for the 'K' smallest as well as for the 'K' biggest elements. +::: diff --git a/docs/src/content/docs/repositories/query-return-types-reference.md b/docs/src/content/docs/repositories/query-return-types-reference.md new file mode 100644 index 000000000..f85fab03b --- /dev/null +++ b/docs/src/content/docs/repositories/query-return-types-reference.md @@ -0,0 +1,45 @@ +--- +title: Repository query return types +description: Reference guide for supported query method return types including collections, reactive types, and pagination +--- + +## Supported Query Return Types + +The following table lists the return types generally supported by Spring Data repositories. +However, consult the store-specific documentation for the exact list of supported return types, because some types listed here might not be supported in a particular store. + +:::note +Geospatial types (such as `GeoResult`, `GeoResults`, and `GeoPage`) are available only for data stores that support geospatial queries. +Some store modules may define their own result wrapper types. +::: + +*Table 1. Query return types* + +| Return type | Description | +|-------------|-------------| +| `void` | Denotes no return value. | +| Primitives | Java primitives. | +| Wrapper types | Java wrapper types. | +| `T` | A unique entity. Expects the query method to return one result at most. If no result is found, `null` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`. | +| `Iterator` | An `Iterator`. | +| `Collection` | A `Collection`. | +| `List` | A `List`. | +| `Optional` | A Java 8 or Guava `Optional`. Expects the query method to return one result at most. If no result is found, `Optional.empty()` or `Optional.absent()` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`. | +| `Option` | Either a Scala or Vavr `Option` type. Semantically the same behavior as Java 8's `Optional`, described earlier. | +| `Stream` | A Java 8 `Stream`. | +| `Streamable` | A convenience extension of `Iterable` that directly exposes methods to stream, map and filter results, concatenate them etc. | +| Types that implement `Streamable` and take a `Streamable` constructor or factory method argument | Types that expose a constructor or `….of(…)`/`….valueOf(…)` factory method taking a `Streamable` as argument. See [Returning Custom Streamable Wrapper Types](/repositories/query-methods-details#returning-custom-streamable-wrapper-types) for details. | +| Vavr `Seq`, `List`, `Map`, `Set` | Vavr collection types. See [Support for Vavr Collections](/repositories/query-methods-details#support-for-vavr-collections) for details. | +| `Future` | A `Future`. Expects a method to be annotated with `@Async` and requires Spring's asynchronous method execution capability to be enabled. | +| `CompletableFuture` | A Java 8 `CompletableFuture`. Expects a method to be annotated with `@Async` and requires Spring's asynchronous method execution capability to be enabled. | +| `Slice` | A sized chunk of data with an indication of whether there is more data available. Requires a `Pageable` method parameter. | +| `Page` | A `Slice` with additional information, such as the total number of results. Requires a `Pageable` method parameter. | +| `Window` | A `Window` of results obtained from a scroll query. Provides `ScrollPosition` to issue the next scroll query. Requires a `ScrollPosition` method parameter. | +| `GeoResult` | A result entry with additional information, such as the distance to a reference location. | +| `GeoResults` | A list of `GeoResult` with additional information, such as the average distance to a reference location. | +| `GeoPage` | A `Page` with `GeoResult`, such as the average distance to a reference location. | +| `Mono` | A Project Reactor `Mono` emitting zero or one element using reactive repositories. Expects the query method to return one result at most. If no result is found, `Mono.empty()` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`. | +| `Flux` | A Project Reactor `Flux` emitting zero, one, or many elements using reactive repositories. Queries returning `Flux` can emit also an infinite number of elements. | +| `Single` | A RxJava `Single` emitting a single element using reactive repositories. Expects the query method to return one result at most. If no result is found, `Mono.empty()` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`. | +| `Maybe` | A RxJava `Maybe` emitting zero or one element using reactive repositories. Expects the query method to return one result at most. If no result is found, `Mono.empty()` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`. | +| `Flowable` | A RxJava `Flowable` emitting zero, one, or many elements using reactive repositories. Queries returning `Flowable` can emit also an infinite number of elements. | diff --git a/docs/src/styles/code-wrap.css b/docs/src/styles/code-wrap.css new file mode 100644 index 000000000..4d574e911 --- /dev/null +++ b/docs/src/styles/code-wrap.css @@ -0,0 +1,6 @@ +/* Make text code blocks word-wrap */ +pre[data-language="text"] .ec-line .code { + white-space: pre-wrap !important; + word-wrap: break-word !important; + overflow-wrap: break-word !important; +} diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css new file mode 100644 index 000000000..19699d322 --- /dev/null +++ b/docs/src/styles/custom.css @@ -0,0 +1,63 @@ +/* Import Open Sans from Google Fonts CDN*/ +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600;1,700&display=swap'); + +/* Define cascade layers - our overrides come after starlight */ +@layer starlight, valkey-overrides; + +@layer valkey-overrides { + +/* Dark mode colors. */ +:root { + --sl-color-accent-low: #192141; + --sl-color-accent: #475ad2; + --sl-color-accent-high: #bac7f2; + --sl-color-white: #ffffff; + --sl-color-gray-1: #eceef2; + --sl-color-gray-2: #c0c2c7; + --sl-color-gray-3: #888b96; + --sl-color-gray-4: #545861; + --sl-color-gray-5: #353841; + --sl-color-gray-6: #24272f; + --sl-color-black: #17181c; + --vk-color-main-hsl: 236, 83%, 63%; + --overlay-color: hsla(var(--vk-color-main-hsl), 0.2); + --sl-font: 'Open Sans', system-ui, sans-serif; + --sl-font-system: 'Open Sans', system-ui, sans-serif; +} + +/* Light mode colors. */ +:root[data-theme='light'] { + --sl-color-accent-low: #ccd6f6; + --sl-color-accent: #485cd5; + --sl-color-accent-high: #222d61; + --sl-color-white: #17181c; + --sl-color-gray-1: #24272f; + --sl-color-gray-2: #353841; + --sl-color-gray-3: #545861; + --sl-color-gray-4: #888b96; + --sl-color-gray-5: #c0c2c7; + --sl-color-gray-6: #eceef2; + --sl-color-gray-7: #f5f6f8; + --sl-color-black: #ffffff; + --vk-color-main-hsl: 236, 83%, 63%; +} + +[data-has-hero] .page { + background: + linear-gradient(var(--sl-color-black) 25%, var(--overlay-color) 75%) +} + +[data-has-hero] .hero > img { + filter: drop-shadow(0 0 3rem var(--overlay-color)); +} + /* Apply rounded corner */ + .starlight-aside, + .card { + border-radius: 0.75rem; + } +} + +.hero { + padding-bottom: 1.25rem; + padding-top: 1.25rem; +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 000000000..8bf91d3bb --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/spring-data-valkey/pom.xml b/spring-data-valkey/pom.xml index ea6fe5086..3734193e3 100644 --- a/spring-data-valkey/pom.xml +++ b/spring-data-valkey/pom.xml @@ -504,27 +504,5 @@ - - antora-process-resources - - - - src/main/antora/resources/antora-resources - true - - - - - - antora - - - - org.antora - antora-maven-plugin - - - - From 88542925aa9154910ccde2a92a6c3fff9e581193 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Mon, 26 Jan 2026 13:58:17 -0800 Subject: [PATCH 02/21] Remove Antora doc files Signed-off-by: Jeremy Parr-Pearson --- .../src/main/antora/antora-playbook.yml | 40 -- spring-data-valkey/src/main/antora/antora.yml | 17 - .../antora/modules/ROOT/examples/examples | 1 - .../src/main/antora/modules/ROOT/nav.adoc | 49 --- .../antora/modules/ROOT/pages/appendix.adoc | 208 ---------- .../modules/ROOT/pages/commons/upgrade.adoc | 6 - .../main/antora/modules/ROOT/pages/index.adoc | 20 - .../modules/ROOT/pages/observability.adoc | 107 ----- .../antora/modules/ROOT/pages/preface.adoc | 83 ---- .../main/antora/modules/ROOT/pages/redis.adoc | 43 -- .../modules/ROOT/pages/redis/cluster.adoc | 138 ------- .../ROOT/pages/redis/connection-modes.adoc | 174 -------- .../modules/ROOT/pages/redis/drivers.adoc | 227 ---------- .../ROOT/pages/redis/getting-started.adoc | 41 -- .../ROOT/pages/redis/hash-mappers.adoc | 133 ------ .../ROOT/pages/redis/observability.adoc | 0 .../modules/ROOT/pages/redis/pipelining.adoc | 43 -- .../modules/ROOT/pages/redis/pubsub.adoc | 236 ----------- .../modules/ROOT/pages/redis/redis-cache.adoc | 306 -------------- .../redis/redis-repositories/anatomy.adoc | 141 ------- .../redis-repositories/cdi-integration.adoc | 68 --- .../redis/redis-repositories/cluster.adoc | 35 -- .../redis/redis-repositories/expirations.adoc | 67 --- .../redis/redis-repositories/indexes.adoc | 173 -------- .../redis/redis-repositories/keyspaces.adoc | 60 --- .../redis/redis-repositories/mapping.adoc | 242 ----------- .../redis/redis-repositories/queries.adoc | 78 ---- .../redis-repositories/query-by-example.adoc | 46 --- .../pages/redis/redis-repositories/usage.adoc | 161 -------- .../ROOT/pages/redis/redis-streams.adoc | 332 --------------- .../modules/ROOT/pages/redis/scripting.adoc | 78 ---- .../ROOT/pages/redis/support-classes.adoc | 67 --- .../modules/ROOT/pages/redis/template.adoc | 386 ------------------ .../ROOT/pages/redis/transactions.adoc | 120 ------ .../modules/ROOT/pages/repositories.adoc | 15 - .../pages/repositories/core-concepts.adoc | 4 - .../repositories/core-domain-events.adoc | 1 - .../pages/repositories/core-extensions.adoc | 4 - .../pages/repositories/create-instances.adoc | 1 - .../repositories/custom-implementations.adoc | 1 - .../ROOT/pages/repositories/definition.adoc | 1 - .../pages/repositories/null-handling.adoc | 1 - .../pages/repositories/object-mapping.adoc | 1 - .../ROOT/pages/repositories/projections.adoc | 4 - .../query-keywords-reference.adoc | 1 - .../repositories/query-methods-details.adoc | 1 - .../query-return-types-reference.adoc | 1 - .../antora/modules/ROOT/pages/upgrading.adoc | 268 ------------ .../resources/antora-resources/antora.yml | 23 -- 49 files changed, 4253 deletions(-) delete mode 100644 spring-data-valkey/src/main/antora/antora-playbook.yml delete mode 100644 spring-data-valkey/src/main/antora/antora.yml delete mode 120000 spring-data-valkey/src/main/antora/modules/ROOT/examples/examples delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/nav.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/appendix.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/index.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/observability.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/preface.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/cluster.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/drivers.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/observability.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pipelining.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/anatomy.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/indexes.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/keyspaces.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/queries.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/query-by-example.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/scripting.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/template.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/transactions.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/definition.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/object-mapping.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/projections.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc delete mode 100644 spring-data-valkey/src/main/antora/modules/ROOT/pages/upgrading.adoc delete mode 100644 spring-data-valkey/src/main/antora/resources/antora-resources/antora.yml diff --git a/spring-data-valkey/src/main/antora/antora-playbook.yml b/spring-data-valkey/src/main/antora/antora-playbook.yml deleted file mode 100644 index b90d6ab32..000000000 --- a/spring-data-valkey/src/main/antora/antora-playbook.yml +++ /dev/null @@ -1,40 +0,0 @@ -# PACKAGES antora@3.2.0-alpha.2 @antora/atlas-extension:1.0.0-alpha.1 @antora/collector-extension@1.0.0-alpha.3 @springio/antora-extensions@1.1.0-alpha.2 @asciidoctor/tabs@1.0.0-alpha.12 @opendevise/antora-release-line-extension@1.0.0-alpha.2 -# -# The purpose of this Antora playbook is to build the docs in the current branch. -antora: - extensions: - - require: '@springio/antora-extensions' - root_component_name: 'data-redis' -site: - title: Spring Data Redis - url: https://docs.spring.io/spring-data-redis/reference/ -content: - sources: - - url: ./../../.. - branches: HEAD - start_path: src/main/antora - worktrees: true - - url: https://github.com/spring-projects/spring-data-commons - # Refname matching: - # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ main, 3.2.x ] - start_path: src/main/antora -asciidoc: - attributes: - hide-uri-scheme: '@' - tabs-sync-option: '@' - extensions: - - '@asciidoctor/tabs' - - '@springio/asciidoctor-extensions' - - '@springio/asciidoctor-extensions/javadoc-extension' - sourcemap: true -urls: - latest_version_segment: '' -runtime: - log: - failure_level: warn - format: pretty -ui: - bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip - snapshot: true diff --git a/spring-data-valkey/src/main/antora/antora.yml b/spring-data-valkey/src/main/antora/antora.yml deleted file mode 100644 index f2cc6bda1..000000000 --- a/spring-data-valkey/src/main/antora/antora.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: data-redis -version: true -title: Spring Data Redis -nav: - - modules/ROOT/nav.adoc -ext: - collector: - - run: - command: ./mvnw validate process-resources -Pantora-process-resources - local: true - scan: - dir: target/classes/ - - run: - command: ./mvnw package -Pdistribute - local: true - scan: - dir: target/antora diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/examples/examples b/spring-data-valkey/src/main/antora/modules/ROOT/examples/examples deleted file mode 120000 index 3fb279766..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/examples/examples +++ /dev/null @@ -1 +0,0 @@ -../../../../../test/java/org/springframework/data/redis/examples \ No newline at end of file diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/nav.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/nav.adoc deleted file mode 100644 index 7cc4db98c..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/nav.adoc +++ /dev/null @@ -1,49 +0,0 @@ -* xref:index.adoc[Overview] -** xref:commons/upgrade.adoc[] -** xref:upgrading.adoc[] - - -* xref:redis.adoc[] -** xref:redis/getting-started.adoc[] -** xref:redis/drivers.adoc[] -** xref:redis/connection-modes.adoc[] -** xref:redis/template.adoc[RedisTemplate] -** xref:redis/redis-cache.adoc[] -** xref:redis/cluster.adoc[] -** xref:redis/hash-mappers.adoc[] -** xref:redis/pubsub.adoc[] -** xref:redis/redis-streams.adoc[] -** xref:redis/scripting.adoc[] -** xref:redis/transactions.adoc[] -** xref:redis/pipelining.adoc[] -** xref:redis/support-classes.adoc[] - -* xref:repositories.adoc[] -** xref:repositories/core-concepts.adoc[] -** xref:repositories/definition.adoc[] -** xref:repositories/create-instances.adoc[] -** xref:redis/redis-repositories/usage.adoc[] -** xref:repositories/object-mapping.adoc[] -** xref:redis/redis-repositories/mapping.adoc[] -** xref:redis/redis-repositories/keyspaces.adoc[] -** xref:redis/redis-repositories/indexes.adoc[] -** xref:redis/redis-repositories/expirations.adoc[] -** xref:redis/redis-repositories/queries.adoc[] -** xref:redis/redis-repositories/query-by-example.adoc[] -** xref:redis/redis-repositories/cluster.adoc[] -** xref:redis/redis-repositories/anatomy.adoc[] -** xref:repositories/projections.adoc[] -** xref:repositories/custom-implementations.adoc[] -** xref:repositories/core-domain-events.adoc[] -** xref:repositories/null-handling.adoc[] -** xref:redis/redis-repositories/cdi-integration.adoc[] -** xref:repositories/query-keywords-reference.adoc[] -** xref:repositories/query-return-types-reference.adoc[] - -* xref:observability.adoc[] - -* xref:appendix.adoc[] - - -* xref:attachment$api/java/index.html[Javadoc,role=link-external,window=_blank] -* https://github.com/spring-projects/spring-data-commons/wiki[Wiki,role=link-external,window=_blank] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/appendix.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/appendix.adoc deleted file mode 100644 index 669bf8220..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/appendix.adoc +++ /dev/null @@ -1,208 +0,0 @@ -[[appendix]] -= Appendix - -[[schema]] -== Schema - -link:https://www.springframework.org/schema/redis/spring-redis-1.0.xsd[Spring Data Redis Schema (redis-namespace)] - -[[supported-commands]] -== Supported Commands - -.Redis commands supported by `RedisTemplate` -[width="50%",cols="<2,^1",options="header"] -|========================================================= -|Command |Template Support - -|APPEND |X -|AUTH |X -|BGREWRITEAOF |X -|BGSAVE |X -|BITCOUNT |X -|BITFIELD |X -|BITOP |X -|BLPOP |X -|BRPOP |X -|BRPOPLPUSH |X -|CLIENT KILL |X -|CLIENT GETNAME |X -|CLIENT LIST |X -|CLIENT SETNAME |X -|CLUSTER SLOTS |- -|COMMAND |- -|COMMAND COUNT |- -|COMMAND GETKEYS |- -|COMMAND INFO |- -|CONFIG GET |X -|CONFIG RESETSTAT |X -|CONFIG REWRITE |- -|CONFIG SET |X -|DBSIZE |X -|DEBUG OBJECT |- -|DEBUG SEGFAULT |- -|DECR |X -|DECRBY |X -|DEL |X -|DISCARD |X -|DUMP |X -|ECHO |X -|EVAL |X -|EVALSHA |X -|EXEC |X -|EXISTS |X -|EXPIRE |X -|EXPIREAT |X -|FLUSHALL |X -|FLUSHDB |X -|GEOADD |X -|GEODIST |X -|GEOHASH |X -|GEOPOS |X -|GEORADIUS |X -|GEORADIUSBYMEMBER |X -|GEOSEARCH |X -|GEOSEARCHSTORE |X -|GET |X -|GETBIT |X -|GETRANGE |X -|GETSET |X -|HDEL |X -|HEXISTS |X -|HEXPIRE |X -|HEXPIREAT |X -|HPEXPIRE |X -|HPEXPIREAT |X -|HPERSIST |X -|HTTL |X -|HPTTL |X -|HGET |X -|HGETALL |X -|HINCRBY |X -|HINCRBYFLOAT |X -|HKEYS |X -|HLEN |X -|HMGET |X -|HMSET |X -|HSCAN |X -|HSET |X -|HSETNX |X -|HVALS |X -|INCR |X -|INCRBY |X -|INCRBYFLOAT |X -|INFO |X -|KEYS |X -|LASTSAVE |X -|LINDEX |X -|LINSERT |X -|LLEN |X -|LPOP |X -|LPUSH |X -|LPUSHX |X -|LRANGE |X -|LREM |X -|LSET |X -|LTRIM |X -|MGET |X -|MIGRATE |- -|MONITOR |- -|MOVE |X -|MSET |X -|MSETNX |X -|MULTI |X -|OBJECT |- -|PERSIST |X -|PEXIPRE |X -|PEXPIREAT |X -|PFADD |X -|PFCOUNT |X -|PFMERGE |X -|PING |X -|PSETEX |X -|PSUBSCRIBE |X -|PTTL |X -|PUBLISH |X -|PUBSUB |- -|PUBSUBSCRIBE |- -|QUIT |X -|RANDOMKEY |X -|RENAME |X -|RENAMENX |X -|REPLICAOF |X -|RESTORE |X -|ROLE |- -|RPOP |X -|RPOPLPUSH |X -|RPUSH |X -|RPUSHX |X -|SADD |X -|SAVE |X -|SCAN |X -|SCARD |X -|SCRIPT EXITS |X -|SCRIPT FLUSH |X -|SCRIPT KILL |X -|SCRIPT LOAD |X -|SDIFF |X -|SDIFFSTORE |X -|SELECT |X -|SENTINEL FAILOVER |X -|SENTINEL GET-MASTER-ADD-BY-NAME |- -|SENTINEL MASTER | - -|SENTINEL MASTERS |X -|SENTINEL MONITOR |X -|SENTINEL REMOVE |X -|SENTINEL RESET |- -|SENTINEL SET |- -|SENTINEL SLAVES |X -|SET |X -|SETBIT |X -|SETEX |X -|SETNX |X -|SETRANGE |X -|SHUTDOWN |X -|SINTER |X -|SINTERSTORE |X -|SISMEMBER |X -|SLAVEOF |X -|SLOWLOG |- -|SMEMBERS |X -|SMOVE |X -|SORT |X -|SPOP |X -|SRANDMEMBER |X -|SREM |X -|SSCAN |X -|STRLEN |X -|SUBSCRIBE |X -|SUNION |X -|SUNIONSTORE |X -|SYNC |- -|TIME |X -|TTL |X -|TYPE |X -|UNSUBSCRIBE |X -|UNWATCH |X -|WATCH |X -|ZADD |X -|ZCARD |X -|ZCOUNT |X -|ZINCRBY |X -|ZINTERSTORE |X -|ZLEXCOUNT |- -|ZRANGE |X -|ZRANGEBYLEX |- -|ZREVRANGEBYLEX |- -|ZRANGEBYSCORE |X -|ZRANGESTORE |X -|ZRANK |X -|ZREM |X -|ZREMRANGEBYLEX |- -|ZREMRANGEBYRANK |X -|ZREVRANGE |X -|ZREVRANGEBYSCORE |X -|ZREVRANK |X -|ZSCAN |X -|ZSCORE |X -|ZUNINONSTORE |X -|========================================================= diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc deleted file mode 100644 index 59a572776..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/commons/upgrade.adoc +++ /dev/null @@ -1,6 +0,0 @@ -include::{commons}@data-commons::page$upgrade.adoc[] - -Once you’ve decided to upgrade your application, you can find detailed information regarding specific features in the rest of the document. -You can find xref:upgrading.adoc#redis.upgrading[migration guides] specific to major version migrations at the end of this document. - -Spring Data's documentation is specific to that version, so any information that you find in here will contain the most up-to-date changes that are in that version. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/index.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/index.adoc deleted file mode 100644 index c0ce7e607..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/index.adoc +++ /dev/null @@ -1,20 +0,0 @@ -[[spring-data-redis-reference-documentation]] -= Spring Data Redis -:revnumber: {version} -:revdate: {localdate} -:feature-scroll: true - -_Spring Data Redis provides Redis connectivity and repository support for the Redis database. -It eases development of applications with a consistent programming model that need to access Redis data sources._ - -[horizontal] -xref:redis.adoc[Redis] :: Redis support and connectivity -xref:repositories.adoc[Repositories] :: Redis Repositories -xref:observability.adoc[Observability] :: Observability Integration -https://github.com/spring-projects/spring-data-commons/wiki[Wiki] :: What's New, Upgrade Notes, Supported Versions, additional cross-version information. - -Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch, Jay Bryant - -(C) 2008-{copyright-year} VMware, Inc. - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/observability.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/observability.adoc deleted file mode 100644 index e3a43ae12..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/observability.adoc +++ /dev/null @@ -1,107 +0,0 @@ -[[redis.observability]] -= Observability - -Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. -Spring Data Redis ships with a Micrometer integration through the Lettuce driver to collect observations during Redis interaction. -Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Redis command. - -To enable the integration, apply the following configuration to `LettuceClientConfiguration`: - -[source,java] ----- -@Configuration -class ObservabilityConfiguration { - - @Bean - public ClientResources clientResources(ObservationRegistry observationRegistry) { - - return ClientResources.builder() - .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) - .build(); - } - - @Bean - public LettuceConnectionFactory lettuceConnectionFactory(ClientResources clientResources) { - - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - .clientResources(clientResources).build(); - RedisConfiguration redisConfiguration = …; - return new LettuceConnectionFactory(redisConfiguration, clientConfig); - } -} ----- - -See also https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis[OpenTelemetry Semantic Conventions] for further reference. - -[[observability-metrics]] -== Observability - Metrics - -Below you can find a list of all metrics declared by this project. - -[[observability-metrics-redis-command-observation]] -== Redis Command Observation - -____ -Timer created around a Redis command execution. -____ - -**Metric name** `spring.data.redis`. **Type** `timer` and **base unit** `seconds`. - -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. - - - -.Low cardinality Keys -[cols="a,a"] -|=== -|Name | Description -|`db.operation`|Redis command value. -|`db.redis.database_index`|Redis database index. -|`db.system`|Database system. -|`db.user`|Redis user. -|`net.peer.name`|Name of the database host. -|`net.peer.port`|Logical remote port number. -|`net.sock.peer.addr`|Mongo peer address. -|`net.sock.peer.port`|Mongo peer port. -|`net.transport`|Network transport. -|=== - -.High cardinality Keys -[cols="a,a"] -|=== -|Name | Description -|`db.statement`|Redis statement. -|`spring.data.redis.command.error`|Redis error response. -|=== - -[[observability-spans]] -== Observability - Spans - -Below you can find a list of all spans declared by this project. - -[[observability-spans-redis-command-observation]] -== Redis Command Observation Span - -> Timer created around a Redis command execution. - -**Span name** `spring.data.redis`. - -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. - - - -.Tag Keys -|=== -|Name | Description -|`db.operation`|Redis command value. -|`db.redis.database_index`|Redis database index. -|`db.statement`|Redis statement. -|`db.system`|Database system. -|`db.user`|Redis user. -|`net.peer.name`|Name of the database host. -|`net.peer.port`|Logical remote port number. -|`net.sock.peer.addr`|Mongo peer address. -|`net.sock.peer.port`|Mongo peer port. -|`net.transport`|Network transport. -|`spring.data.redis.command.error`|Redis error response. -|=== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/preface.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/preface.adoc deleted file mode 100644 index 7b536361e..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/preface.adoc +++ /dev/null @@ -1,83 +0,0 @@ -[[preface]] -= Preface - -The Spring Data Redis project applies core Spring concepts to the development of solutions by using a key-value style data store. -We provide a "`template`" as a high-level abstraction for sending and receiving messages. -You may notice similarities to the JDBC support in the Spring Framework. - -This section provides an easy-to-follow guide for getting started with the Spring Data Redis module. - -[[get-started:first-steps:spring]] -== Learning Spring - -Spring Data uses Spring framework's -{spring-framework-docs}/core.html[core] functionality, including: - - -* {spring-framework-docs}/core.html#beans[IoC] container -* {spring-framework-docs}/core.html#validation[type conversion system] -* {spring-framework-docs}/core.html#expressions[expression language] -* {spring-framework-docs}/integration.html#jmx[JMX integration] -* {spring-framework-docs}/data-access.html#dao-exceptions[DAO exception hierarchy]. - -While you need not know the Spring APIs, understanding the concepts behind them is important. -At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. - -The core functionality of the Redis support can be used directly, with no need to invoke the IoC services of the Spring Container. -This is much like `JdbcTemplate`, which can be used "'standalone'" without any other services of the Spring container. -To leverage all the features of Spring Data Redis, such as the repository support, you need to configure some parts of the library to use Spring. - -To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. -There are a lot of articles, blog entries, and books on the subject. -See the Spring framework https://spring.io/projects/spring-framework/[home page] for more information. - -In general, this should be the starting point for developers wanting to try Spring Data Redis. - -[[get-started:first-steps:nosql]] -== Learning NoSQL and Key Value Stores - -NoSQL stores have taken the storage world by storm. -It is a vast domain with a plethora of solutions, terms, and patterns (to make things worse, even the term itself has multiple https://www.google.com/search?q=nosoql+acronym[meanings]). -While some of the principles are common, it is crucial that you be familiar to some degree with the stores supported by SDR. The best way to get acquainted with these solutions is to read their documentation and follow their examples. -It usually does not take more then five to ten minutes to go through them and, if you come from an RDMBS-only background, many times these exercises can be eye-openers. - -[[get-started:first-steps:samples]] -=== Trying out the Samples - -One can find various samples for key-value stores in the dedicated Spring Data example repo, at https://github.com/spring-projects/spring-data-examples/tree/main/redis[https://github.com/spring-projects/spring-data-examples/]. - -[[requirements]] -== Requirements - -Spring Data Redis binaries require JDK level 17 and above and https://spring.io/projects/spring-framework/[Spring Framework] {springVersion} and above. - -In terms of key-value stores, https://redis.io[Redis] 2.6.x or higher is required. -Spring Data Redis is currently tested against the latest 6.0 release. - -[[get-started:help]] -== Additional Help Resources - -Learning a new framework is not always straightforward. -In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data Redis module. -However, if you encounter issues or you need advice, feel free to use one of the following links: - -[get-started:help:community]] -Community Forum :: Spring Data on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow] is a tag for all Spring Data (not just Document) users to share information and help each other. -Note that registration is needed only for posting. - -[[get-started:help:professional]] -Professional Support :: Professional, from-the-source support, with guaranteed response time, is available from https://pivotal.io/[Pivotal Sofware, Inc.], the company behind Spring Data and Spring. - -[[get-started:up-to-date]] -== Following Development - -For information on the Spring Data source code repository, nightly builds, and snapshot artifacts, see the Spring Data home https://spring.io/projects/spring-data/[page]. - -You can help make Spring Data best serve the needs of the Spring community by interacting with developers on Stack Overflow at either -https://stackoverflow.com/questions/tagged/spring-data[spring-data] or https://stackoverflow.com/questions/tagged/spring-data-redis[spring-data-redis]. - -If you encounter a bug or want to suggest an improvement (including to this documentation), please create a ticket on https://github.com/spring-projects/spring-data-redis/issues/new[Github]. - -To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community https://spring.io/[Portal]. - -Lastly, you can follow the Spring https://spring.io/blog/[blog] or the project team (https://twitter.com/SpringData[@SpringData]) on Twitter. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis.adoc deleted file mode 100644 index c00c5a8ae..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis.adoc +++ /dev/null @@ -1,43 +0,0 @@ -[[redis]] -= Redis -:page-section-summary-toc: 1 - -One of the key-value stores supported by Spring Data is https://redis.io[Redis]. -To quote the Redis project home page: - -[quote] -Redis is an advanced key-value store. -It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. -All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. -Redis supports different kind of sorting abilities. - -Spring Data Redis provides easy configuration and access to Redis from Spring applications. -It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns. - -Spring Data support for Redis contains a wide range of features: - -* xref:redis/template.adoc[`RedisTemplate` and `ReactiveRedisTemplate` helper class] that increases productivity when performing common Redis operations. -Includes integrated serialization between objects and values. -* Exception translation into Spring's portable Data Access Exception hierarchy. -* Automatic implementation of xref:repositories.adoc[Repository interfaces], including support for custom query methods. -* Feature-rich xref:redis/redis-repositories/mapping.adoc[Object Mapping] integrated with Spring's Conversion Service. -* Annotation-based mapping metadata that is extensible to support other metadata formats. -* xref:redis/transactions.adoc[Transactions] and xref:redis/pipelining.adoc[Pipelining]. -* xref:redis/redis-cache.adoc[Redis Cache] integration through Spring's Cache abstraction. -* xref:redis/pubsub.adoc[Redis Pub/Sub Messaging] and xref:redis/redis-streams.adoc[Redis Stream] Listeners. -* xref:redis/support-classes.adoc[Redis Collection Implementations] for Java such as `RedisList` or `RedisSet`. - -== Why Spring Data Redis? - -The Spring Framework is the leading full-stack Java/JEE application framework. -It provides a lightweight container and a non-invasive programming model enabled by the use of dependency injection, AOP, and portable service abstractions. - -https://en.wikipedia.org/wiki/NoSQL[NoSQL] storage systems provide an alternative to classical RDBMS for horizontal scalability and speed. -In terms of implementation, key-value stores represent one of the largest (and oldest) members in the NoSQL space. - -The Spring Data Redis (SDR) framework makes it easy to write Spring applications that use the Redis key-value store by eliminating the redundant tasks and boilerplate code required for interacting with the store through Spring's excellent infrastructure support. - -[[redis:architecture]] -== Redis Support High-level View - -The Redis support provides several components.For most tasks, the high-level abstractions and support services are the best choice.Note that, at any point, you can move between layers.For example, you can get a low-level connection (or even the native library) to communicate directly with Redis. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/cluster.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/cluster.adoc deleted file mode 100644 index 9e1e18f68..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/cluster.adoc +++ /dev/null @@ -1,138 +0,0 @@ -[[cluster]] -= Redis Cluster - -Working with https://redis.io/topics/cluster-spec[Redis Cluster] requires Redis Server version 3.0+. -See the https://redis.io/topics/cluster-tutorial[Cluster Tutorial] for more information. - -NOTE: When using xref:repositories.adoc[Redis Repositories] with Redis Cluster, make yourself familiar with how to xref:redis/redis-repositories/cluster.adoc[run Redis Repositories on a Cluster]. - -CAUTION: Do not rely on keyspace events when using Redis Cluster as keyspace events are not replicated across shards. -Pub/Sub https://github.com/spring-projects/spring-data-redis/issues/1111[subscribes to a random cluster node] which only receives keyspace events from a single shard. -Use single-node Redis to avoid keyspace event loss. - -[[cluster.working.with.cluster]] -== Working With Redis Cluster Connection - -Redis Cluster behaves differently from single-node Redis or even a Sentinel-monitored master-replica environment. -This is because the automatic sharding maps a key to one of `16384` slots, which are distributed across the nodes. -Therefore, commands that involve more than one key must assert all keys map to the exact same slot to avoid cross-slot errors. -A single cluster node serves only a dedicated set of keys. -Commands issued against one particular server return results only for those keys served by that server. -As a simple example, consider the `KEYS` command. -When issued to a server in a cluster environment, it returns only the keys served by the node the request is sent to and not necessarily all keys within the cluster. -So, to get all keys in a cluster environment, you must read the keys from all the known master nodes. - -While redirects for specific keys to the corresponding slot-serving node are handled by the driver libraries, higher-level functions, such as collecting information across nodes or sending commands to all nodes in the cluster, are covered by `RedisClusterConnection`. -Picking up the keys example from earlier, this means that the `keys(pattern)` method picks up every master node in the cluster and simultaneously runs the `KEYS` command on every master node while picking up the results and returning the cumulated set of keys. -To just request the keys of a single node `RedisClusterConnection` provides overloads for those methods (for example, `keys(node, pattern)`). - -A `RedisClusterNode` can be obtained from `RedisClusterConnection.clusterGetNodes` or it can be constructed by using either the host and the port or the node Id. - -The following example shows a set of commands being run across the cluster: - -.Sample of Running Commands Across the Cluster -==== -[source,text] ----- -redis-cli@127.0.0.1:7379 > cluster nodes - -6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 <1> -7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-20252 <2> -164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-20253 <3> -b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected <4> ----- - -[source,java] ----- -RedisClusterConnection connection = connectionFactory.getClusterConnection(); - -connection.set("thing1", value); <5> -connection.set("thing2", value); <6> - -connection.keys("*"); <7> - -connection.keys(NODE_7379, "*"); <8> -connection.keys(NODE_7380, "*"); <9> -connection.keys(NODE_7381, "*"); <10> -connection.keys(NODE_7382, "*"); <11> ----- - -<1> Master node serving slots 0 to 5460 replicated to replica at 7382 -<2> Master node serving slots 5461 to 10922 -<3> Master node serving slots 10923 to 16383 -<4> Replica node holding replicants of the master at 7379 -<5> Request routed to node at 7381 serving slot 12182 -<6> Request routed to node at 7379 serving slot 5061 -<7> Request routed to nodes at 7379, 7380, 7381 -> [thing1, thing2] -<8> Request routed to node at 7379 -> [thing2] -<9> Request routed to node at 7380 -> [] -<10> Request routed to node at 7381 -> [thing1] -<11> Request routed to node at 7382 -> [thing2] -==== - -When all keys map to the same slot, the native driver library automatically serves cross-slot requests, such as `MGET`. -However, once this is not the case, `RedisClusterConnection` runs multiple parallel `GET` commands against the slot-serving nodes and again returns an accumulated result. -This is less performant than the single-slot approach and, therefore, should be used with care. -If in doubt, consider pinning keys to the same slot by providing a prefix in curly brackets, such as `\{my-prefix}.thing1` and `\{my-prefix}.thing2`, which will both map to the same slot number. -The following example shows cross-slot request handling: - -.Sample of Cross-Slot Request Handling -==== -[source,text] ----- -redis-cli@127.0.0.1:7379 > cluster nodes - -6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 <1> -7bb... ----- - -[source,java] ----- -RedisClusterConnection connection = connectionFactory.getClusterConnection(); - -connection.set("thing1", value); // slot: 12182 -connection.set("{thing1}.thing2", value); // slot: 12182 -connection.set("thing2", value); // slot: 5461 - -connection.mGet("thing1", "{thing1}.thing2"); <2> - -connection.mGet("thing1", "thing2"); <3> ----- - -<1> Same Configuration as in the sample before. -<2> Keys map to same slot -> 127.0.0.1:7381 MGET thing1 \{thing1}.thing2 -<3> Keys map to different slots and get split up into single slot ones routed to the according nodes + --> 127.0.0.1:7379 GET thing2 + --> 127.0.0.1:7381 GET thing1 -==== - -TIP: The preceding examples demonstrate the general strategy followed by Spring Data Redis. -Be aware that some operations might require loading huge amounts of data into memory to compute the desired command. -Additionally, not all cross-slot requests can safely be ported to multiple single slot requests and error if misused (for example, `PFCOUNT`). - -[[cluster.redistemplate]] -== Working with `RedisTemplate` and `ClusterOperations` - -See the xref:redis/template.adoc[Working with Objects through RedisTemplate] section for information about the general purpose, configuration, and usage of `RedisTemplate`. - -CAUTION: Be careful when setting up `RedisTemplate#keySerializer` using any of the JSON `RedisSerializers`, as changing JSON structure has immediate influence on hash slot calculation. - -`RedisTemplate` provides access to cluster-specific operations through the `ClusterOperations` interface, which can be obtained from `RedisTemplate.opsForCluster()`. -This lets you explicitly run commands on a single node within the cluster while retaining the serialization and deserialization features configured for the template. -It also provides administrative commands (such as `CLUSTER MEET`) or more high-level operations (for example, resharding). - -The following example shows how to access `RedisClusterConnection` with `RedisTemplate`: - -.Accessing `RedisClusterConnection` with `RedisTemplate` -==== -[source,java] ----- -ClusterOperations clusterOps = redisTemplate.opsForCluster(); -clusterOps.shutdown(NODE_7379); <1> ----- - -<1> Shut down node at 7379 and cross fingers there is a replica in place that can take over. -==== - -NOTE: Redis Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. -Same-slot keys are fully supported. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc deleted file mode 100644 index 2a8f11623..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/connection-modes.adoc +++ /dev/null @@ -1,174 +0,0 @@ -[[configuration]] -= Connection Modes - -Redis can be operated in various setups. -Each mode of operation requires specific configuration that is explained in the following sections. - -[[redis:standalone]] -== Redis Standalone - -The easiest way to get started is by using Redis Standalone with a single Redis server, - -Configure javadoc:org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory[] or javadoc:org.springframework.data.redis.connection.jedis.JedisConnectionFactory[], as shown in the following example: - -[source,java] ----- -@Configuration -class RedisStandaloneConfiguration { - - /** - * Lettuce - */ - @Bean - public RedisConnectionFactory lettuceConnectionFactory() { - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); - } - - /** - * Jedis - */ - @Bean - public RedisConnectionFactory jedisConnectionFactory() { - return new JedisConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); - } -} ----- - -[[redis:write-to-master-read-from-replica]] -== Write to Master, Read from Replica - -The Redis Master/Replica setup -- without automatic failover (for automatic failover see: <>) -- not only allows data to be safely stored at more nodes. -It also allows, by using xref:redis/drivers.adoc#redis:connectors:lettuce[Lettuce], reading data from replicas while pushing writes to the master. -You can set the read/write strategy to be used by using `LettuceClientConfiguration`, as shown in the following example: - -[source,java] ----- -@Configuration -class WriteToMasterReadFromReplicaConfiguration { - - @Bean - public LettuceConnectionFactory redisConnectionFactory() { - - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - .readFrom(REPLICA_PREFERRED) - .build(); - - RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("server", 6379); - - return new LettuceConnectionFactory(serverConfig, clientConfig); - } -} ----- - -TIP: For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use javadoc:org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration[] instead of javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[]. Please note that `RedisStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. - -[[redis:sentinel]] -== Redis Sentinel - -For dealing with high-availability Redis, Spring Data Redis has support for https://redis.io/topics/sentinel[Redis Sentinel], using javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], as shown in the following example: - -[source,java] ----- -/** - * Lettuce - */ -@Bean -public RedisConnectionFactory lettuceConnectionFactory() { - RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() - .master("mymaster") - .sentinel("127.0.0.1", 26379) - .sentinel("127.0.0.1", 26380); - return new LettuceConnectionFactory(sentinelConfig); -} - -/** - * Jedis - */ -@Bean -public RedisConnectionFactory jedisConnectionFactory() { - RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() - .master("mymaster") - .sentinel("127.0.0.1", 26379) - .sentinel("127.0.0.1", 26380); - return new JedisConnectionFactory(sentinelConfig); -} ----- - -[TIP] -==== -`RedisSentinelConfiguration` can also be defined through `RedisSentinelConfiguration.of(PropertySource)`, which lets you pick up the following properties: - -.Configuration Properties -* `spring.redis.sentinel.master`: name of the master node. -* `spring.redis.sentinel.nodes`: Comma delimited list of host:port pairs. -* `spring.redis.sentinel.username`: The username to apply when authenticating with Redis Sentinel (requires Redis 6) -* `spring.redis.sentinel.password`: The password to apply when authenticating with Redis Sentinel -* `spring.redis.sentinel.dataNode.username`: The username to apply when authenticating with Redis Data Node -* `spring.redis.sentinel.dataNode.password`: The password to apply when authenticating with Redis Data Node -* `spring.redis.sentinel.dataNode.database`: The database index to apply when authenticating with Redis Data Node -==== - -Sometimes, direct interaction with one of the Sentinels is required. Using `RedisConnectionFactory.getSentinelConnection()` or `RedisConnection.getSentinelCommands()` gives you access to the first active Sentinel configured. - -[[cluster.enable]] -== Redis Cluster - -xref:redis/cluster.adoc[Cluster support] is based on the same building blocks as non-clustered communication. javadoc:org.springframework.data.redis.connection.RedisClusterConnection[], an extension to `RedisConnection`, handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. -`RedisClusterConnection` instances are created with the `RedisConnectionFactory`, which has to be set up with the associated javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[], as shown in the following example: - -.Sample RedisConnectionFactory Configuration for Redis Cluster -==== -[source,java] ----- -@Component -@ConfigurationProperties(prefix = "spring.redis.cluster") -public class ClusterConfigurationProperties { - - /* - * spring.redis.cluster.nodes[0] = 127.0.0.1:7379 - * spring.redis.cluster.nodes[1] = 127.0.0.1:7380 - * ... - */ - List nodes; - - /** - * Get initial collection of known cluster nodes in format {@code host:port}. - * - * @return - */ - public List getNodes() { - return nodes; - } - - public void setNodes(List nodes) { - this.nodes = nodes; - } -} - -@Configuration -public class AppConfig { - - /** - * Type safe representation of application.properties - */ - @Autowired ClusterConfigurationProperties clusterProperties; - - public @Bean RedisConnectionFactory connectionFactory() { - - return new LettuceConnectionFactory( - new RedisClusterConfiguration(clusterProperties.getNodes())); - } -} ----- -==== - -[TIP] -==== -`RedisClusterConfiguration` can also be defined through `RedisClusterConfiguration.of(PropertySource)`, which lets you pick up the following properties: - -.Configuration Properties -- `spring.redis.cluster.nodes`: Comma-delimited list of host:port pairs. -- `spring.redis.cluster.max-redirects`: Number of allowed cluster redirections. -==== - -NOTE: The initial configuration points driver libraries to an initial set of cluster nodes. Changes resulting from live cluster reconfiguration are kept only in the native driver and are not written back to the configuration. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/drivers.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/drivers.adoc deleted file mode 100644 index 52570144d..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/drivers.adoc +++ /dev/null @@ -1,227 +0,0 @@ -[[redis:connectors]] -= Drivers - -One of the first tasks when using Redis and Spring is to connect to the store through the IoC container. -To do that, a Java connector (or binding) is required. -No matter the library you choose, you need to use only one set of Spring Data Redis APIs (which behaves consistently across all connectors). -The `org.springframework.data.redis.connection` package and its `RedisConnection` and `RedisConnectionFactory` interfaces for working with and retrieving active connections to Redis. - -[[redis:connectors:connection]] -== RedisConnection and RedisConnectionFactory - -`RedisConnection` provides the core building block for Redis communication, as it handles the communication with the Redis backend. -It also automatically translates underlying connecting library exceptions to Spring's consistent {spring-framework-docs}/data-access.html#dao-exceptions[DAO exception hierarchy] so that you can switch connectors without any code changes, as the operation semantics remain the same. - -NOTE: For the corner cases where the native library API is required, `RedisConnection` provides a dedicated method (`getNativeConnection`) that returns the raw, underlying object used for communication. - -Active `RedisConnection` objects are created through `RedisConnectionFactory`. -In addition, the factory acts as `PersistenceExceptionTranslator` objects, meaning that, once declared, they let you do transparent exception translation. -For example, you can do exception translation through the use of the `@Repository` annotation and AOP. -For more information, see the dedicated {spring-framework-docs}/data-access.html#orm-exception-translation[section] in the Spring Framework documentation. - -NOTE: `RedisConnection` classes are **not** Thread-safe. -While the underlying native connection, such as Lettuce's `StatefulRedisConnection`, may be Thread-safe, Spring Data Redis's `LettuceConnection` class itself is not. -Therefore, you should **not** share instances of a `RedisConnection` across multiple Threads. -This is especially true for transactional, or blocking Redis operations and commands, such as `BLPOP`. -In transactional and pipelining operations, for instance, `RedisConnection` holds onto unguarded mutable state to complete the operation correctly, thereby making it unsafe to use with multiple Threads. -This is by design. - -TIP: If you need to share (stateful) Redis resources, like connections, across multiple Threads, for performance reasons or otherwise, then you should acquire the native connection and use the Redis client library (driver) API directly. -Alternatively, you can use the `RedisTemplate`, which acquires and manages connections for operations (and Redis commands) in a Thread-safe manner. -See xref:redis/template.adoc[documentation] on `RedisTemplate` for more details. - -NOTE: Depending on the underlying configuration, the factory can return a new connection or an existing connection (when a pool or shared native connection is used). - -The easiest way to work with a `RedisConnectionFactory` is to configure the appropriate connector through the IoC container and inject it into the using class. - -Unfortunately, currently, not all connectors support all Redis features. -When invoking a method on the Connection API that is unsupported by the underlying library, an `UnsupportedOperationException` is thrown. -The following overview explains features that are supported by the individual Redis connectors: - -[[redis:connectors:overview]] -.Feature Availability across Redis Connectors -|=== -| Supported Feature | Lettuce | Jedis - -| Standalone Connections -| X -| X - -| xref:redis.adoc#redis:write-to-master-read-from-replica[Master/Replica Connections] -| X -| - -| xref:redis.adoc#redis:sentinel[Redis Sentinel] -| Master Lookup, Sentinel Authentication, Replica Reads -| Master Lookup - -| xref:redis/cluster.adoc[Redis Cluster] -| Cluster Connections, Cluster Node Connections, Replica Reads -| Cluster Connections, Cluster Node Connections - -| Transport Channels -| TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets -| TCP - -| Connection Pooling -| X (using `commons-pool2`) -| X (using `commons-pool2`) - -| Other Connection Features -| Singleton-connection sharing for non-blocking commands -| Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. - -| SSL Support -| X -| X - -| xref:redis/pubsub.adoc[Pub/Sub] -| X -| X - -| xref:redis/pipelining.adoc[Pipelining] -| X -| X (Pipelining and Transactions mutually exclusive) - -| xref:redis/transactions.adoc[Transactions] -| X -| X (Pipelining and Transactions mutually exclusive) - -| Datatype support -| Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog -| Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog - -| Reactive (non-blocking) API -| X -| - -|=== - -[[redis:connectors:lettuce]] -== Configuring the Lettuce Connector - -https://github.com/lettuce-io/lettuce-core[Lettuce] is a https://netty.io/[Netty]-based open-source connector supported by Spring Data Redis through the `org.springframework.data.redis.connection.lettuce` package. - -.Add the following to the pom.xml files `dependencies` element: -[source,xml,subs="+attributes"] ----- - - - - - - io.lettuce - lettuce-core - {lettuce} - - - ----- - -The following example shows how to create a new Lettuce connection factory: - -[source,java] ----- -@Configuration -class AppConfig { - - @Bean - public LettuceConnectionFactory redisConnectionFactory() { - - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); - } -} ----- - -There are also a few Lettuce-specific connection parameters that can be tweaked. -By default, all `LettuceConnection` instances created by the `LettuceConnectionFactory` share the same thread-safe native connection for all non-blocking and non-transactional operations. -To use a dedicated connection each time, set `shareNativeConnection` to `false`. `LettuceConnectionFactory` can also be configured to use a `LettucePool` for pooling blocking and transactional connections or all connections if `shareNativeConnection` is set to `false`. - -The following example shows a more sophisticated configuration, including SSL and timeouts, that uses `LettuceClientConfigurationBuilder`: - -[source,java] ----- -@Bean -public LettuceConnectionFactory lettuceConnectionFactory() { - - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - .useSsl().and() - .commandTimeout(Duration.ofSeconds(2)) - .shutdownTimeout(Duration.ZERO) - .build(); - - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379), clientConfig); -} ----- - -For more detailed client configuration tweaks, see javadoc:org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration[]. - -Lettuce integrates with Netty's https://netty.io/wiki/native-transports.html[native transports], letting you use Unix domain sockets to communicate with Redis. -Make sure to include the appropriate native transport dependencies that match your runtime environment. -The following example shows how to create a Lettuce Connection factory for a Unix domain socket at `/var/run/redis.sock`: - -[source,java] ----- -@Configuration -class AppConfig { - - @Bean - public LettuceConnectionFactory redisConnectionFactory() { - - return new LettuceConnectionFactory(new RedisSocketConfiguration("/var/run/redis.sock")); - } -} ----- - -NOTE: Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for OS-native transport. - -[[redis:connectors:jedis]] -== Configuring the Jedis Connector - -https://github.com/redis/jedis[Jedis] is a community-driven connector supported by the Spring Data Redis module through the `org.springframework.data.redis.connection.jedis` package. - -.Add the following to the pom.xml files `dependencies` element: -[source,xml,subs="+attributes"] ----- - - - - - - redis.clients - jedis - {jedis} - - - ----- - -In its simplest form, the Jedis configuration looks as follow: - -[source,java] ----- -@Configuration -class AppConfig { - - @Bean - public JedisConnectionFactory redisConnectionFactory() { - return new JedisConnectionFactory(); - } -} ----- - -For production use, however, you might want to tweak settings such as the host or password, as shown in the following example: - -[source,java] ----- -@Configuration -class RedisConfiguration { - - @Bean - public JedisConnectionFactory redisConnectionFactory() { - - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("server", 6379); - return new JedisConnectionFactory(config); - } -} ----- diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc deleted file mode 100644 index 3a803ad85..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/getting-started.adoc +++ /dev/null @@ -1,41 +0,0 @@ -[[redis.getting-started]] -= Getting Started - -An easy way to bootstrap setting up a working environment is to create a Spring-based project via https://start.spring.io/#!type=maven-project&dependencies=data-redis[start.spring.io] or create a Spring project in https://spring.io/tools[Spring Tools]. - -[[redis.examples-repo]] -== Examples Repository - -The GitHub https://github.com/spring-projects/spring-data-examples[spring-data-examples repository] hosts several examples that you can download and play around with to get a feel for how the library works. - -[[redis.hello-world]] -== Hello World - -First, you need to set up a running Redis server. -Spring Data Redis requires Redis 2.6 or above and Spring Data Redis integrates with https://github.com/lettuce-io/lettuce-core[Lettuce] and https://github.com/redis/jedis[Jedis], two popular open-source Java libraries for Redis. - -Now you can create a simple Java application that stores and reads a value to and from Redis. - -Create the main application to run, as the following example shows: - -[tabs] -====== -Imperative:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -include::example$examples/RedisApplication.java[tags=file] ----- - -Reactive:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="secondary"] ----- -include::example$examples/ReactiveRedisApplication.java[tags=file] ----- -====== - -Even in this simple example, there are a few notable things to point out: - -* You can create an instance of javadoc:org.springframework.data.redis.core.RedisTemplate[] (or javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[]for reactive usage) with a javadoc:org.springframework.data.redis.connection.RedisConnectionFactory[]. Connection factories are an abstraction on top of the supported drivers. -* There's no single way to use Redis as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc deleted file mode 100644 index 334a2fd51..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc +++ /dev/null @@ -1,133 +0,0 @@ -[[redis.hashmappers.root]] -= Hash Mapping - -Data can be stored by using various data structures within Redis. javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] can convert objects in https://en.wikipedia.org/wiki/JSON[JSON] format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): - -* Direct mapping, by using javadoc:org.springframework.data.redis.core.HashOperations[] and a xref:redis.adoc#redis:serializer[serializer] -* Using xref:repositories.adoc[Redis Repositories] -* Using javadoc:org.springframework.data.redis.hash.HashMapper[] and javadoc:org.springframework.data.redis.core.HashOperations[] - -[[redis.hashmappers.mappers]] -== Hash Mappers - -Hash mappers are converters of map objects to a `Map` and back. javadoc:org.springframework.data.redis.hash.HashMapper[] is intended for using with Redis Hashes. - -Multiple implementations are available: - -* javadoc:org.springframework.data.redis.hash.BeanUtilsHashMapper[] using Spring's {spring-framework-javadoc}/org/springframework/beans/BeanUtils.html[BeanUtils]. -* javadoc:org.springframework.data.redis.hash.ObjectHashMapper[] using xref:redis/redis-repositories/mapping.adoc[Object-to-Hash Mapping]. -* <> using https://github.com/FasterXML/jackson[FasterXML Jackson]. - -The following example shows one way to implement hash mapping: - -[source,java] ----- -public class Person { - String firstname; - String lastname; - - // … -} - -public class HashMapping { - - @Resource(name = "redisTemplate") - HashOperations hashOperations; - - HashMapper mapper = new ObjectHashMapper(); - - public void writeHash(String key, Person person) { - - Map mappedHash = mapper.toHash(person); - hashOperations.putAll(key, mappedHash); - } - - public Person loadHash(String key) { - - Map loadedHash = hashOperations.entries(key); - return (Person) mapper.fromHash(loadedHash); - } -} ----- - -[[redis.hashmappers.jackson2]] -=== Jackson2HashMapper - -javadoc:org.springframework.data.redis.hash.Jackson2HashMapper[] provides Redis Hash mapping for domain objects by using https://github.com/FasterXML/jackson[FasterXML Jackson]. -`Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. -Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. - -Flattening creates individual hash entries for all nested properties and resolves complex types into simple types, as far as possible. - -Consider the following class and the data structure it contains: - -[source,java] ----- -public class Person { - String firstname; - String lastname; - Address address; - Date date; - LocalDateTime localDateTime; -} - -public class Address { - String city; - String country; -} ----- - -The following table shows how the data in the preceding class would appear in normal mapping: - -.Normal Mapping -[width="80%",cols="<1,<2",options="header"] -|==== -|Hash Field -|Value - -|firstname -|`Jon` - -|lastname -|`Snow` - -|address -|`{ "city" : "Castle Black", "country" : "The North" }` - -|date -|`1561543964015` - -|localDateTime -|`2018-01-02T12:13:14` -|==== - -The following table shows how the data in the preceding class would appear in flat mapping: - -.Flat Mapping -[width="80%",cols="<1,<2",options="header"] -|==== -|Hash Field -|Value - -|firstname -|`Jon` - -|lastname -|`Snow` - -|address.city -|`Castle Black` - -|address.country -|`The North` - -|date -|`1561543964015` - -|localDateTime -|`2018-01-02T12:13:14` -|==== - -NOTE: Flattening requires all property names to not interfere with the JSON path. Using dots or brackets in map keys or as property names is not supported when you use flattening. The resulting hash cannot be mapped back into an Object. - -NOTE: `java.util.Date` and `java.util.Calendar` are represented with milliseconds. JSR-310 Date/Time types are serialized to their `toString` form if `jackson-datatype-jsr310` is on the class path. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/observability.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/observability.adoc deleted file mode 100644 index e69de29bb..000000000 diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pipelining.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pipelining.adoc deleted file mode 100644 index 7e5ddd054..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pipelining.adoc +++ /dev/null @@ -1,43 +0,0 @@ -[[pipeline]] -= Pipelining - -Redis provides support for https://redis.io/topics/pipelining[pipelining], which involves sending multiple commands to the server without waiting for the replies and then reading the replies in a single step. Pipelining can improve performance when you need to send several commands in a row, such as adding many elements to the same List. - -Spring Data Redis provides several `RedisTemplate` methods for running commands in a pipeline. If you do not care about the results of the pipelined operations, you can use the standard `execute` method, passing `true` for the `pipeline` argument. The `executePipelined` methods run the provided `RedisCallback` or `SessionCallback` in a pipeline and return the results, as shown in the following example: - -[source,java] ----- -//pop a specified number of items from a queue -List results = stringRedisTemplate.executePipelined( - new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - StringRedisConnection stringRedisConn = (StringRedisConnection)connection; - for(int i=0; i< batchSize; i++) { - stringRedisConn.rPop("myqueue"); - } - return null; - } -}); ----- - -The preceding example runs a bulk right pop of items from a queue in a pipeline. -The `results` `List` contains all the popped items. `RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results before returning, so the returned items in the preceding example are Strings. -There are additional `executePipelined` methods that let you pass a custom serializer for pipelined results. - -Note that the value returned from the `RedisCallback` is required to be `null`, as this value is discarded in favor of returning the results of the pipelined commands. - -[TIP] -==== -The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. - -[source,java] ----- -LettuceConnectionFactory factory = // ... -factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3)); <1> ----- -<1> Buffer locally and flush after every 3rd command. -==== - -NOTE: Pipelining is limited to Redis Standalone. -Redis Cluster is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. -Same-slot keys are fully supported. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc deleted file mode 100644 index 031425a4e..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/pubsub.adoc +++ /dev/null @@ -1,236 +0,0 @@ -[[pubsub]] -= Pub/Sub Messaging - -Spring Data provides dedicated messaging integration for Redis, similar in functionality and naming to the JMS integration in Spring Framework. - -Redis messaging can be roughly divided into two areas of functionality: - -* Publication or production of messages -* Subscription or consumption of messages - -This is an example of the pattern often called Publish/Subscribe (Pub/Sub for short). The `RedisTemplate` class is used for message production. For asynchronous reception similar to Java EE's message-driven bean style, Spring Data provides a dedicated message listener container that is used to create Message-Driven POJOs (MDPs) and, for synchronous reception, the `RedisConnection` contract. - -The `org.springframework.data.redis.connection` and `org.springframework.data.redis.listener` packages provide the core functionality for Redis messaging. - -[[redis:pubsub:publish]] -== Publishing (Sending Messages) - -To publish a message, you can use, as with the other operations, either the low-level `[Reactive]RedisConnection` or the high-level `[Reactive]RedisOperations`. Both entities offer the `publish` method, which accepts the message and the destination channel as arguments. While `RedisConnection` requires raw data (array of bytes), the `[Reactive]RedisOperations` lets arbitrary objects be passed in as messages, as shown in the following example: - -[tabs] -====== -Imperative:: -+ -[source,java,role="primary"] ----- -// send message through connection -RedisConnection con = … -byte[] msg = … -byte[] channel = … -con.pubSubCommands().publish(msg, channel); - -// send message through RedisOperations -RedisOperations operations = … -Long numberOfClients = operations.convertAndSend("hello!", "world"); ----- - -Reactive:: -+ -[source,java,role="secondary"] ----- -// send message through connection -ReactiveRedisConnection con = … -ByteBuffer[] msg = … -ByteBuffer[] channel = … -con.pubSubCommands().publish(msg, channel); - -// send message through ReactiveRedisOperations -ReactiveRedisOperations operations = … -Mono numberOfClients = operations.convertAndSend("hello!", "world"); ----- -====== - -[[redis:pubsub:subscribe]] -== Subscribing (Receiving Messages) - -On the receiving side, one can subscribe to one or multiple channels either by naming them directly or by using pattern matching. The latter approach is quite useful, as it not only lets multiple subscriptions be created with one command but can also listen on channels not yet created at subscription time (as long as they match the pattern). - -At the low-level, `RedisConnection` offers the `subscribe` and `pSubscribe` methods that map the Redis commands for subscribing by channel or by pattern, respectively. Note that multiple channels or patterns can be used as arguments. To change the subscription of a connection or query whether it is listening, `RedisConnection` provides the `getSubscription` and `isSubscribed` methods. - -NOTE: Subscription commands in Spring Data Redis are blocking. That is, calling subscribe on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the subscription is canceled, which happens when another thread invokes `unsubscribe` or `pUnsubscribe` on the *same* connection. See "`xref:redis/pubsub.adoc#redis:pubsub:subscribe:containers[Message Listener Containers]`" (later in this document) for a solution to this problem. - -As mentioned earlier, once subscribed, a connection starts waiting for messages. Only commands that add new subscriptions, modify existing subscriptions, and cancel existing subscriptions are allowed. Invoking anything other than `subscribe`, `pSubscribe`, `unsubscribe`, or `pUnsubscribe` throws an exception. - -In order to subscribe to messages, one needs to implement the `MessageListener` callback. Each time a new message arrives, the callback gets invoked and the user code gets run by the `onMessage` method. The interface gives access not only to the actual message but also to the channel it has been received through and the pattern (if any) used by the subscription to match the channel. This information lets the callee differentiate between various messages not just by content but also examining additional details. - -[[redis:pubsub:subscribe:containers]] -=== Message Listener Containers - -Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[], which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). - -javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] acts as a message listener container. It is used to receive messages from a Redis channel and drive the javadoc:org.springframework.data.redis.connection.MessageListener[] instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. - -A javadoc:org.springframework.data.redis.connection.MessageListener[] can additionally implement javadoc:org.springframework.data.redis.connection.SubscriptionListener[] to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. - -Furthermore, to minimize the application footprint, javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[] lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. - -To help with the asynchronous nature of messages, the container requires a `java.util.concurrent.Executor` (or Spring's `TaskExecutor`) for dispatching the messages. Depending on the load, the number of listeners, or the runtime environment, you should change or tweak the executor to better serve your needs. In particular, in managed environments (such as app servers), it is highly recommended to pick a proper `TaskExecutor` to take advantage of its runtime. - - -[[redis:pubsub:subscribe:adapter]] -=== The MessageListenerAdapter - -The javadoc:org.springframework.data.redis.listener.adapter.MessageListenerAdapter[] class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). - -Consider the following interface definition: - -[source,java] ----- -public interface MessageDelegate { - void handleMessage(String message); - void handleMessage(Map message); - void handleMessage(byte[] message); - void handleMessage(Serializable message); - // pass the channel/pattern as well - void handleMessage(Serializable message, String channel); - } ----- - -Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the javadoc:org.springframework.data.redis.listener.adapter.MessageListenerAdapter[] class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: - -[source,java] ----- -public class DefaultMessageDelegate implements MessageDelegate { - // implementation elided for clarity... -} ----- - - Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Redis dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -class MyConfig { - - // … - - @Bean - DefaultMessageDelegate listener() { - return new DefaultMessageDelegate(); - } - - @Bean - MessageListenerAdapter messageListenerAdapter(DefaultMessageDelegate listener) { - return new MessageListenerAdapter(listener, "handleMessage"); - } - - @Bean - RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) { - - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - container.addMessageListener(listener, ChannelTopic.of("chatroom")); - return container; - } -} ----- - -XML:: -+ -[source,xml,role="secondary"] ----- - - - - - - - - - - - ... - ----- -====== - -NOTE: The listener topic can be either a channel (for example, `topic="chatroom"` respective `Topic.channel("chatroom")`) or a pattern (for example, `topic="*room"` respective `Topic.pattern("*room")`). - -The preceding example uses the Redis namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: - -[source,xml] ----- - - - - - - - - - - - - - - - - - - ----- - -Each time a message is received, the adapter automatically and transparently performs translation (using the configured `RedisSerializer`) between the low-level format and the required object type. Any exception caused by the method invocation is caught and handled by the container (by default, exceptions get logged). - -[[redis:reactive:pubsub:subscribe:containers]] -== Reactive Message Listener Container - -Spring Data offers javadoc:org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer[] which does all the heavy lifting of conversion and subscription state management on behalf of the user. - -The message listener container itself does not require external threading resources. It uses the driver threads to publish messages. - -[source,java] ----- -ReactiveRedisConnectionFactory factory = … -ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); - -Flux> stream = container.receive(ChannelTopic.of("my-channel")); ----- - -To await and ensure proper subscription, you can use the `receiveLater` method that returns a `Mono>`. -The resulting `Mono` completes with an inner publisher as a result of completing the subscription to the given topics. By intercepting `onNext` signals, you can synchronize server-side subscriptions. - -[source,java] ----- -ReactiveRedisConnectionFactory factory = … -ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); - -Mono>> stream = container.receiveLater(ChannelTopic.of("my-channel")); - -stream.doOnNext(inner -> // notification hook when Redis subscriptions are synchronized with the server) - .flatMapMany(Function.identity()) - .…; ----- - -[[redis:reactive:pubsub:subscribe:template]] -=== Subscribing via template API - -As mentioned above you can directly use javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[] to subscribe to channels / patterns. This approach -offers a straight forward, though limited solution as you lose the option to add subscriptions after the initial -ones. Nevertheless you still can control the message stream via the returned `Flux` using eg. `take(Duration)`. When -done reading, on error or cancellation all bound resources are freed again. - -[source,java] ----- -redisTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> { - // message processing ... -}).subscribe(); ----- diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc deleted file mode 100644 index 4e9f48df5..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-cache.adoc +++ /dev/null @@ -1,306 +0,0 @@ -[[redis:support:cache-abstraction]] -= Redis Cache - -Spring Data Redis provides an implementation of Spring Framework's {spring-framework-docs}/integration.html#cache[Cache Abstraction] in the `org.springframework.data.redis.cache` package. -To use Redis as a backing implementation, add javadoc:org.springframework.data.redis.cache.RedisCacheManager[] to your configuration, as follows: - -[source,java] ----- -@Bean -public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { - return RedisCacheManager.create(connectionFactory); -} ----- - -`RedisCacheManager` behavior can be configured with javadoc:org.springframework.data.redis.cache.RedisCacheManager$RedisCacheManagerBuilder[], letting you set the default javadoc:org.springframework.data.redis.cache.RedisCacheManager[], transaction behavior, and predefined caches. - -[source,java] ----- -RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - .transactionAware() - .withInitialCacheConfigurations(Collections.singletonMap("predefined", - RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues())) - .build(); ----- - -As shown in the preceding example, `RedisCacheManager` allows custom configuration on a per-cache basis. - -The behavior of javadoc:org.springframework.data.redis.cache.RedisCache[] created by javadoc:org.springframework.data.redis.cache.RedisCacheManager[] is defined with `RedisCacheConfiguration`. -The configuration lets you set key expiration times, prefixes, and `RedisSerializer` implementations for converting to and from the binary storage format, as shown in the following example: - -[source,java] ----- -RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofSeconds(1)) - .disableCachingNullValues(); ----- - -javadoc:org.springframework.data.redis.cache.RedisCacheManager[] defaults to a lock-free javadoc:org.springframework.data.redis.cache.RedisCacheWriter[] for reading and writing binary values. -Lock-free caching improves throughput. -The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Redis. -The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. - -Locking applies on the *cache level*, not per *cache entry*. - -It is possible to opt in to the locking behavior as follows: - -[source,java] ----- -RedisCacheManager cacheManager = RedisCacheManager - .builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - ... ----- - -By default, any `key` for a cache entry gets prefixed with the actual cache name followed by two colons (`::`). -This behavior can be changed to a static as well as a computed prefix. - -The following example shows how to set a static prefix: - -[source,java] ----- -// static key prefix -RedisCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)"); - -The following example shows how to set a computed prefix: - -// computed key prefix -RedisCacheConfiguration.defaultCacheConfig() - .computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName); ----- - -The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `KEYS` can cause performance issues with large keyspaces. -Therefore, the default `RedisCacheWriter` can be created with a `BatchStrategy` to switch to a `SCAN`-based batch strategy. -The `SCAN` strategy requires a batch size to avoid excessive Redis command round trips: - -[source,java] ----- -RedisCacheManager cacheManager = RedisCacheManager - .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - ... ----- - -[NOTE] -==== -The `KEYS` batch strategy is fully supported using any driver and Redis operation mode (Standalone, Clustered). -`SCAN` is fully supported when using the Lettuce driver. -Jedis supports `SCAN` only in non-clustered modes. -==== - -The following table lists the default settings for `RedisCacheManager`: - -.`RedisCacheManager` defaults -[width="80%",cols="<1,<2",options="header"] -|==== -|Setting -|Value - -|Cache Writer -|Non-locking, `KEYS` batch strategy - -|Cache Configuration -|`RedisCacheConfiguration#defaultConfiguration` - -|Initial Caches -|None - -|Transaction Aware -|No -|==== - -The following table lists the default settings for `RedisCacheConfiguration`: - -.RedisCacheConfiguration defaults -[width="80%",cols="<1,<2",options="header"] -|==== -|Key Expiration -|None - -|Cache `null` -|Yes - -|Prefix Keys -|Yes - -|Default Prefix -|The actual cache name - -|Key Serializer -|`StringRedisSerializer` - -|Value Serializer -|`JdkSerializationRedisSerializer` - -|Conversion Service -|`DefaultFormattingConversionService` with default cache key converters -|==== - -[NOTE] -==== -By default `RedisCache`, statistics are disabled. -Use `RedisCacheManagerBuilder.enableStatistics()` to collect local _hits_ and _misses_ through `RedisCache#getStatistics()`, returning a snapshot of the collected data. -==== - -[[redis:support:cache-abstraction:expiration]] -== Redis Cache Expiration - -The implementation of time-to-idle (TTI) as well as time-to-live (TTL) varies in definition and behavior even across different data stores. - -In general: - -* _time-to-live_ (TTL) _expiration_ - TTL is only set and reset by a create or update data access operation. -As long as the entry is written before the TTL expiration timeout, including on creation, an entry's timeout will reset to the configured duration of the TTL expiration timeout. -For example, if the TTL expiration timeout is set to 5 minutes, then the timeout will be set to 5 minutes on entry creation and reset to 5 minutes anytime the entry is updated thereafter and before the 5-minute interval expires. -If no update occurs within 5 minutes, even if the entry was read several times, or even just read once during the 5-minute interval, the entry will still expire. -The entry must be written to prevent the entry from expiring when declaring a TTL expiration policy. - -* _time-to-idle_ (TTI) _expiration_ - TTI is reset anytime the entry is also read as well as for entry updates, and is effectively and extension to the TTL expiration policy. - -[NOTE] -==== -Some data stores expire an entry when TTL is configured no matter what type of data access operation occurs on the entry (reads, writes, or otherwise). -After the set, configured TTL expiration timeout, the entry is evicted from the data store regardless. -Eviction actions (for example: destroy, invalidate, overflow-to-disk (for persistent stores), etc.) are data store specific. -==== - -[[redis:support:cache-abstraction:expiration:tti]] -=== Time-To-Live (TTL) Expiration - -Spring Data Redis's `Cache` implementation supports _time-to-live_ (TTL) expiration on cache entries. -Users can either configure the TTL expiration timeout with a fixed `Duration` or a dynamically computed `Duration` per cache entry by supplying an implementation of the new `RedisCacheWriter.TtlFunction` interface. - -[TIP] -==== -The `RedisCacheWriter.TtlFunction` interface was introduced in Spring Data Redis `3.2.0`. -==== - -If all cache entries should expire after a set duration of time, then simply configure a TTL expiration timeout with a fixed `Duration`, as follows: - -[source,java] ----- -RedisCacheConfiguration fiveMinuteTtlExpirationDefaults = - RedisCacheConfiguration.defaultCacheConfig().enableTtl(Duration.ofMinutes(5)); ----- - -However, if the TTL expiration timeout should vary by cache entry, then you must provide a custom implementation of the `RedisCacheWriter.TtlFunction` interface: - -[source,java] ----- -enum MyCustomTtlFunction implements TtlFunction { - - INSTANCE; - - @Override - public Duration getTimeToLive(Object key, @Nullable Object value) { - // compute a TTL expiration timeout (Duration) based on the cache entry key and/or value - } -} ----- - -[NOTE] -==== -Under-the-hood, a fixed `Duration` TTL expiration is wrapped in a `TtlFunction` implementation returning the provided `Duration`. -==== - -Then, you can either configure the fixed `Duration` or the dynamic, per-cache entry `Duration` TTL expiration on a global basis using: - -.Global fixed Duration TTL expiration timeout -[source,java] ----- -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(fiveMinuteTtlExpirationDefaults) - .build(); ----- - -Or, alternatively: - -.Global, dynamically computed per-cache entry Duration TTL expiration timeout -[source,java] ----- -RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(MyCustomTtlFunction.INSTANCE); - -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(defaults) - .build(); ----- - -Of course, you can combine both global and per-cache configuration using: - -.Global fixed Duration TTL expiration timeout -[source,java] ----- - -RedisCacheConfiguration predefined = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(MyCustomTtlFunction.INSTANCE); - -Map initialCaches = Collections.singletonMap("predefined", predefined); - -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(fiveMinuteTtlExpirationDefaults) - .withInitialCacheConfigurations(initialCaches) - .build(); ----- - -[[redis:support:cache-abstraction:expiration:tti2]] -=== Time-To-Idle (TTI) Expiration - -Redis itself does not support the concept of true, time-to-idle (TTI) expiration. -Still, using Spring Data Redis's Cache implementation, it is possible to achieve time-to-idle (TTI) expiration-like behavior. - -The configuration of TTI in Spring Data Redis's Cache implementation must be explicitly enabled, that is, is opt-in. -Additionally, you must also provide TTL configuration using either a fixed `Duration` or a custom implementation of the `TtlFunction` interface as described above in <>. - -For example: - -[source,java] ----- -@Configuration -@EnableCaching -class RedisConfiguration { - - @Bean - RedisConnectionFactory redisConnectionFactory() { - // ... - } - - @Bean - RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { - - RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofMinutes(5)) - .enableTimeToIdle(); - - return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(defaults) - .build(); - } -} ----- - -Because Redis servers do not implement a proper notion of TTI, then TTI can only be achieved with Redis commands accepting expiration options. -In Redis, the "expiration" is technically a time-to-live (TTL) policy. -However, TTL expiration can be passed when reading the value of a key thereby effectively resetting the TTL expiration timeout, as is now the case in Spring Data Redis's `Cache.get(key)` operation. - -`RedisCache.get(key)` is implemented by calling the Redis `GETEX` command. - -[WARNING] -==== -The Redis https://redis.io/commands/getex[`GETEX`] command is only available in Redis version `6.2.0` and later. -Therefore, if you are not using Redis `6.2.0` or later, then it is not possible to use Spring Data Redis's TTI expiration. -A command execution exception will be thrown if you enable TTI against an incompatible Redis (server) version. -No attempt is made to determine if the Redis server version is correct and supports the `GETEX` command. -==== - -[WARNING] -==== -In order to achieve true time-to-idle (TTI) expiration-like behavior in your Spring Data Redis application, then an entry must be consistently accessed with (TTL) expiration on every read or write operation. -There are no exceptions to this rule. -If you are mixing and matching different data access patterns across your Spring Data Redis application (for example: caching, invoking operations using `RedisTemplate` and possibly, or especially when using Spring Data Repository CRUD operations), then accessing an entry may not necessarily prevent the entry from expiring if TTL expiration was set. -For example, an entry maybe "put" in (written to) the cache during a `@Cacheable` service method invocation with a TTL expiration (i.e. `SET `) and later read using a Spring Data Redis Repository before the expiration timeout (using `GET` without expiration options). -A simple `GET` without specifying expiration options will not reset the TTL expiration timeout on an entry. -Therefore, the entry may expire before the next data access operation, even though it was just read. -Since this cannot be enforced in the Redis server, then it is the responsibility of your application to consistently access an entry when time-to-idle expiration is configured, in and outside of caching, where appropriate. -==== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/anatomy.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/anatomy.adoc deleted file mode 100644 index 6724a1876..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/anatomy.adoc +++ /dev/null @@ -1,141 +0,0 @@ -[[redis.repositories.anatomy]] -= Redis Repositories Anatomy - -Redis as a store itself offers a very narrow low-level API leaving higher level functions, such as secondary indexes and query operations, up to the user. - -This section provides a more detailed view of commands issued by the repository abstraction for a better understanding of potential performance implications. - -Consider the following entity class as the starting point for all operations: - -.Example entity -==== -[source,java] ----- -@RedisHash("people") -public class Person { - - @Id String id; - @Indexed String firstname; - String lastname; - Address hometown; -} - -public class Address { - - @GeoIndexed Point location; -} ----- -==== - -[[redis.repositories.anatomy.insert]] -== Insert new - -==== -[source,java] ----- -repository.save(new Person("rand", "al'thor")); ----- - -[source,text] ----- -HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9" "firstname" "rand" "lastname" "al'thor" <1> -SADD "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9" <2> -SADD "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9" <3> -SADD "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand" <4> ----- - -<1> Save the flattened entry as hash. -<2> Add the key of the hash written in <1> to the helper index of entities in the same keyspace. -<3> Add the key of the hash written in <2> to the secondary index of firstnames with the properties value. -<4> Add the index of <3> to the set of helper structures for entry to keep track of indexes to clean on delete/update. -==== - -[[redis.repositories.anatomy.replace]] -== Replace existing - -==== -[source,java] ----- -repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor")); ----- - -[source,text] ----- -DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" <1> -HMSET "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor" <2> -SADD "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" <3> -SMEMBERS "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" <4> -TYPE "people:firstname:rand" <5> -SREM "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" <6> -DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" <7> -SADD "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" <8> -SADD "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn" <9> ----- - -<1> Remove the existing hash to avoid leftovers of hash keys potentially no longer present. -<2> Save the flattened entry as hash. -<3> Add the key of the hash written in <1> to the helper index of entities in the same keyspace. -<4> Get existing index structures that might need to be updated. -<5> Check if the index exists and what type it is (text, geo, …). -<6> Remove a potentially existing key from the index. -<7> Remove the helper holding index information. -<8> Add the key of the hash added in <2> to the secondary index of firstnames with the properties value. -<9> Add the index of <6> to the set of helper structures for entry to keep track of indexes to clean on delete/update. -==== - -[[redis.repositories.anatomy.geo]] -== Save Geo Data - -Geo indexes follow the same rules as normal text based ones but use geo structure to store values. -Saving an entity that uses a Geo-indexed property results in the following commands: - -==== -[source,text] ----- -GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b" <1> -SADD "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location" <2> ----- - -<1> Add the key of the saved entry to the the geo index. -<2> Keep track of the index structure. -==== - -[[redis.repositories.anatomy.index]] -== Find using simple index - -==== -[source,java] ----- -repository.findByFirstname("egwene"); ----- - -[source,text] ----- -SINTER "people:firstname:egwene" <1> -HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3" <2> -HGETALL ... ----- - -<1> Fetch keys contained in the secondary index. -<2> Fetch each key returned by <1> individually. -==== - -[[redis.repositories.anatomy.geo-index]] -== Find using Geo Index - -==== -[source,java] ----- -repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS)); ----- - -[source,text] ----- -GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" <1> -HGETALL "people:76900e94-b057-44bc-abcf-8126d51a621b" <2> -HGETALL ... ----- - -<1> Fetch keys contained in the secondary index. -<2> Fetch each key returned by <1> individually. -==== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc deleted file mode 100644 index bd36cb1c5..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cdi-integration.adoc +++ /dev/null @@ -1,68 +0,0 @@ -[[redis.repositories.cdi-integration]] -= CDI Integration - -Instances of the repository interfaces are usually created by a container, for which Spring is the most natural choice when working with Spring Data. -Spring offers sophisticated for creating bean instances. -Spring Data Redis ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. -The extension is part of the JAR, so, to activate it, drop the Spring Data Redis JAR into your classpath. - -You can then set up the infrastructure by implementing a CDI Producer for the javadoc:org.springframework.data.redis.connection.RedisConnectionFactory[] and javadoc:org.springframework.data.redis.core.RedisOperations[], as shown in the following example: - -[source,java] ----- -class RedisOperationsProducer { - - - @Produces - RedisConnectionFactory redisConnectionFactory() { - - LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration()); - connectionFactory.afterPropertiesSet(); - connectionFactory.start(); - - return connectionFactory; - } - - void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception { - - if (redisConnectionFactory instanceof DisposableBean) { - ((DisposableBean) redisConnectionFactory).destroy(); - } - } - - @Produces - @ApplicationScoped - RedisOperations redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) { - - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - template.afterPropertiesSet(); - - return template; - } - -} ----- - -The necessary setup can vary, depending on your JavaEE environment. - -The Spring Data Redis CDI extension picks up all available repositories as CDI beans and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. -Thus, obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property, as shown in the following example: - -[source,java] ----- -class RepositoryClient { - - @Inject - PersonRepository repository; - - public void businessMethod() { - List people = repository.findAll(); - } -} ----- - -A Redis Repository requires javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter[] and javadoc:org.springframework.data.redis.core.RedisKeyValueTemplate[] instances. -These beans are created and managed by the Spring Data CDI extension if no provided beans are found. -You can, however, supply your own beans to configure the specific properties of javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter[] and javadoc:org.springframework.data.redis.core.RedisKeyValueTemplate[]. - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc deleted file mode 100644 index 2aa5e5eac..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/cluster.adoc +++ /dev/null @@ -1,35 +0,0 @@ -[[redis.repositories.cluster]] -= Redis Repositories Running on a Cluster - -You can use the Redis repository support in a clustered Redis environment. -See the "`xref:redis/cluster.adoc[Redis Cluster]`" section for `ConnectionFactory` configuration details. -Still, some additional configuration must be done, because the default key distribution spreads entities and secondary indexes through out the whole cluster and its slots. - -The following table shows the details of data on a cluster (based on previous examples): - -[options = "header, autowidth"] -|=============== -|Key|Type|Slot|Node -|people:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|15171|127.0.0.1:7381 -|people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|7373|127.0.0.1:7380 -|people:firstname:rand|index|1700|127.0.0.1:7379 -|=============== - -==== - -Some commands (such as `SINTER` and `SUNION`) can only be processed on the server side when all involved keys map to the same slot. -Otherwise, computation has to be done on client side. -Therefore, it is useful to pin keyspaces to a single slot, which lets make use of Redis server side computation right away. -The following table shows what happens when you do (note the change in the slot column and the port value in the node column): - -[options = "header, autowidth"] -|=============== -|Key|Type|Slot|Node -|\{people}:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|2399|127.0.0.1:7379 -|\{people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|2399|127.0.0.1:7379 -|\{people}:firstname:rand|index|2399|127.0.0.1:7379 -|=============== -==== - -TIP: Define and pin keyspaces by using `@RedisHash("\{yourkeyspace}")` to specific slots when you use Redis cluster. - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc deleted file mode 100644 index ddb794213..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/expirations.adoc +++ /dev/null @@ -1,67 +0,0 @@ -[[redis.repositories.expirations]] -= Time To Live - -Objects stored in Redis may be valid only for a certain amount of time. -This is especially useful for persisting short-lived objects in Redis without having to remove them manually when they reach their end of life. -The expiration time in seconds can be set with `@RedisHash(timeToLive=...)` as well as by using javadoc:org.springframework.data.redis.core.convert.KeyspaceConfiguration$KeyspaceSettings[] (see xref:redis/redis-repositories/keyspaces.adoc[Keyspaces]). - -More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or a method. -However, do not apply `@TimeToLive` on both a method and a property within the same class. -The following example shows the `@TimeToLive` annotation on a property and on a method: - -.Expirations -==== -[source,java] ----- -public class TimeToLiveOnProperty { - - @Id - private String id; - - @TimeToLive - private Long expiration; -} - -public class TimeToLiveOnMethod { - - @Id - private String id; - - @TimeToLive - public long getTimeToLive() { - return new Random().nextLong(); - } -} ----- -==== - -NOTE: Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no associated expiration. - -The repository implementation ensures subscription to https://redis.io/topics/notifications[Redis keyspace notifications] via javadoc:org.springframework.data.redis.listener.RedisMessageListenerContainer[]. - -When the expiration is set to a positive value, the corresponding `EXPIRE` command is run. -In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. -This is done to enable the Repository support to publish javadoc:org.springframework.data.redis.core.RedisKeyExpiredEvent[], holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. -Expiry events are received on all connected applications that use Spring Data Redis repositories. - -By default, the key expiry listener is disabled when initializing the application. -The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. -See javadoc:org.springframework.data.redis.core.RedisKeyValueAdapter$EnableKeyspaceEvents[] for possible values. - -The `RedisKeyExpiredEvent` holds a copy of the expired domain object as well as the key. - -NOTE: Delaying or disabling the expiry event listener startup impacts `RedisKeyExpiredEvent` publishing. -A disabled event listener does not publish expiry events. -A delayed startup can cause loss of events because of the delayed listener initialization. - -NOTE: The keyspace notification message listener alters `notify-keyspace-events` settings in Redis, if those are not already set. -Existing settings are not overridden, so you must set up those settings correctly (or leave them empty). -Note that `CONFIG` is disabled on AWS ElastiCache, and enabling the listener leads to an error. -To work around this behavior, set the `keyspaceNotificationsConfigParameter` parameter to an empty string. -This prevents `CONFIG` command usage. - -NOTE: Redis Pub/Sub messages are not persistent. -If a key expires while the application is down, the expiry event is not processed, which may lead to secondary indexes containing references to the expired object. - -NOTE: `@EnableKeyspaceEvents(shadowCopy = OFF)` disable storage of phantom copies and reduces data size within Redis. `RedisKeyExpiredEvent` will only contain the `id` of the expired key. - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/indexes.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/indexes.adoc deleted file mode 100644 index 228f8e92f..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/indexes.adoc +++ /dev/null @@ -1,173 +0,0 @@ -[[redis.repositories.indexes]] -= Secondary Indexes - -https://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. -Values are written to the according indexes on every save and are removed when objects are deleted or xref:redis/redis-repositories/expirations.adoc[expire]. - -[[redis.repositories.indexes.simple]] -== Simple Property Index - -Given the sample `Person` entity shown earlier, we can create an index for `firstname` by annotating the property with `@Indexed`, as shown in the following example: - -.Annotation driven indexing -==== -[source,java] ----- -@RedisHash("people") -public class Person { - - @Id String id; - @Indexed String firstname; - String lastname; - Address address; -} ----- -==== - -Indexes are built up for actual property values. -Saving two Persons (for example, "rand" and "aviendha") results in setting up indexes similar to the following: - -==== -[source,text] ----- -SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e -SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 ----- -==== - -It is also possible to have indexes on nested elements. -Assume `Address` has a `city` property that is annotated with `@Indexed`. -In that case, once `person.address.city` is not `null`, we have Sets for each city, as shown in the following example: - -==== -[source,text] ----- -SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e ----- -==== - -Furthermore, the programmatic setup lets you define indexes on map keys and list properties, as shown in the following example: - -==== -[source,java] ----- -@RedisHash("people") -public class Person { - - // ... other properties omitted - - Map attributes; <1> - Map relatives; <2> - List
addresses; <3> -} ----- - -<1> `SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e` -<2> `SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e` -<3> `SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e` -==== - -CAUTION: Indexes cannot be resolved on xref:redis/redis-repositories/usage.adoc#redis.repositories.references[References]. - -As with keyspaces, you can configure indexes without needing to annotate the actual domain type, as shown in the following example: - -.Index Setup with @EnableRedisRepositories -==== -[source,java] ----- -@Configuration -@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class) -public class ApplicationConfig { - - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - - public static class MyIndexConfiguration extends IndexConfiguration { - - @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new SimpleIndexDefinition("people", "firstname")); - } - } -} ----- -==== - -Again, as with keyspaces, you can programmatically configure indexes, as shown in the following example: - -.Programmatic Index setup -==== -[source,java] ----- -@Configuration -@EnableRedisRepositories -public class ApplicationConfig { - - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - - @Bean - public RedisMappingContext keyValueMappingContext() { - return new RedisMappingContext( - new MappingConfiguration( - new KeyspaceConfiguration(), new MyIndexConfiguration())); - } - - public static class MyIndexConfiguration extends IndexConfiguration { - - @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new SimpleIndexDefinition("people", "firstname")); - } - } -} ----- -==== - -[[redis.repositories.indexes.geospatial]] -== Geospatial Index - -Assume the `Address` type contains a `location` property of type `Point` that holds the geo coordinates of the particular address. -By annotating the property with `@GeoIndexed`, Spring Data Redis adds those values by using Redis `GEO` commands, as shown in the following example: - -==== -[source,java] ----- -@RedisHash("people") -public class Person { - - Address address; - - // ... other properties omitted -} - -public class Address { - - @GeoIndexed Point location; - - // ... other properties omitted -} - -public interface PersonRepository extends CrudRepository { - - List findByAddressLocationNear(Point point, Distance distance); <1> - List findByAddressLocationWithin(Circle circle); <2> -} - -Person rand = new Person("rand", "al'thor"); -rand.setAddress(new Address(new Point(13.361389D, 38.115556D))); - -repository.save(rand); <3> - -repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS)); <4> ----- - -<1> Query method declaration on a nested property, using `Point` and `Distance`. -<2> Query method declaration on a nested property, using `Circle` to search within. -<3> `GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e` -<4> `GEORADIUS people:address:location 15.0 37.0 200.0 km` -==== - -In the preceding example the, longitude and latitude values are stored by using `GEOADD` that use the object's `id` as the member's name. -The finder methods allow usage of `Circle` or `Point, Distance` combinations for querying those values. - -NOTE: It is **not** possible to combine `near` and `within` with other criteria. - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/keyspaces.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/keyspaces.adoc deleted file mode 100644 index 610a9404f..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/keyspaces.adoc +++ /dev/null @@ -1,60 +0,0 @@ -[[redis.repositories.keyspaces]] -= Keyspaces - -Keyspaces define prefixes used to create the actual key for the Redis Hash. -By default, the prefix is set to `getClass().getName()`. -You can alter this default by setting `@RedisHash` on the aggregate root level or by setting up a programmatic configuration. -However, the annotated keyspace supersedes any other configuration. - -The following example shows how to set the keyspace configuration with the `@EnableRedisRepositories` annotation: - -.Keyspace Setup via `@EnableRedisRepositories` -==== -[source,java] ----- -@Configuration -@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class) -public class ApplicationConfig { - - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - - public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { - - @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new KeyspaceSettings(Person.class, "people")); - } - } -} ----- -==== - -The following example shows how to programmatically set the keyspace: - -.Programmatic Keyspace setup -==== -[source,java] ----- -@Configuration -@EnableRedisRepositories -public class ApplicationConfig { - - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - - @Bean - public RedisMappingContext keyValueMappingContext() { - return new RedisMappingContext( - new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration())); - } - - public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { - - @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new KeyspaceSettings(Person.class, "people")); - } - } -} ----- -==== - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc deleted file mode 100644 index fb1a08c0f..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc +++ /dev/null @@ -1,242 +0,0 @@ -[[redis.repositories.mapping]] -= Object-to-Hash Mapping - -The Redis Repository support persists Objects to Hashes. -This requires an Object-to-Hash conversion which is done by a `RedisConverter`. -The default implementation uses `Converter` for mapping property values to and from Redis native `byte[]`. - -Given the `Person` type from the previous sections, the default mapping looks like the following: - -==== -[source,text] ----- -_class = org.example.Person <1> -id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand <2> -lastname = al’thor -address.city = emond's field <3> -address.country = andor ----- - -<1> The `_class` attribute is included on the root level as well as on any nested interface or abstract types. -<2> Simple property values are mapped by path. -<3> Properties of complex types are mapped by their dot path. -==== - -[[mapping-conversion]] -== Data Mapping and Type Conversion - -This section explains how types are mapped to and from a Hash representation: - -[cols="1,2,3",options="header"] -.Default Mapping Rules -|=== -| Type -| Sample -| Mapped Value - -| Simple Type + -(for example, String) -| String firstname = "rand"; -| firstname = "rand" - -| Byte array (`byte[]`) -| byte[] image = "rand".getBytes(); -| image = "rand" - -| Complex Type + -(for example, Address) -| Address address = new Address("emond's field"); -| address.city = "emond's field" - -| List + -of Simple Type -| List nicknames = asList("dragon reborn", "lews therin"); -| nicknames.[0] = "dragon reborn", + -nicknames.[1] = "lews therin" - -| Map + -of Simple Type -| Map atts = asMap({"eye-color", "grey"}, {"... -| atts.[eye-color] = "grey", + -atts.[hair-color] = "... - -| List + -of Complex Type -| List
addresses = asList(new Address("em... -| addresses.[0].city = "emond's field", + -addresses.[1].city = "... - -| Map + -of Complex Type -| Map addresses = asMap({"home", new Address("em... -| addresses.[home].city = "emond's field", + -addresses.[work].city = "... -|=== - -CAUTION: Due to the flat representation structure, Map keys need to be simple types, such as ``String`` or ``Number``. - -Mapping behavior can be customized by registering the corresponding `Converter` in `RedisCustomConversions`. -Those converters can take care of converting from and to a single `byte[]` as well as `Map`. -The first one is suitable for (for example) converting a complex type to (for example) a binary JSON representation that still uses the default mappings hash structure. -The second option offers full control over the resulting hash. - -WARNING: Writing objects to a Redis hash deletes the content from the hash and re-creates the whole hash, so data that has not been mapped is lost. - -The following example shows two sample byte array converters: - -.Sample byte[] Converters -==== -[source,java] ----- -@WritingConverter -public class AddressToBytesConverter implements Converter { - - private final Jackson2JsonRedisSerializer
serializer; - - public AddressToBytesConverter() { - - serializer = new Jackson2JsonRedisSerializer
(Address.class); - serializer.setObjectMapper(new ObjectMapper()); - } - - @Override - public byte[] convert(Address value) { - return serializer.serialize(value); - } -} - -@ReadingConverter -public class BytesToAddressConverter implements Converter { - - private final Jackson2JsonRedisSerializer
serializer; - - public BytesToAddressConverter() { - - serializer = new Jackson2JsonRedisSerializer
(Address.class); - serializer.setObjectMapper(new ObjectMapper()); - } - - @Override - public Address convert(byte[] value) { - return serializer.deserialize(value); - } -} ----- -==== - -Using the preceding byte array `Converter` produces output similar to the following: - -==== -[source,text] ----- -_class = org.example.Person -id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand -lastname = al’thor -address = { city : "emond's field", country : "andor" } ----- -==== - -The following example shows two examples of `Map` converters: - -.Sample Map Converters -==== -[source,java] ----- -@WritingConverter -public class AddressToMapConverter implements Converter> { - - @Override - public Map convert(Address source) { - return singletonMap("ciudad", source.getCity().getBytes()); - } -} - -@ReadingConverter -public class MapToAddressConverter implements Converter, Address> { - - @Override - public Address convert(Map source) { - return new Address(new String(source.get("ciudad"))); - } -} ----- -==== - -Using the preceding Map `Converter` produces output similar to the following: - -==== -[source,text] ----- -_class = org.example.Person -id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand -lastname = al’thor -ciudad = "emond's field" ----- -==== - -NOTE: Custom conversions have no effect on index resolution. xref:redis/redis-repositories/indexes.adoc[Secondary Indexes] are still created, even for custom converted types. - -[[customizing-type-mapping]] -== Customizing Type Mapping - -If you want to avoid writing the entire Java class name as type information and would rather like to use a key, you can use the `@TypeAlias` annotation on the entity class being persisted. -If you need to customize the mapping even more, look at the https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/convert/TypeInformationMapper.html[`TypeInformationMapper`] interface. -An instance of that interface can be configured at the `DefaultRedisTypeMapper`, which can be configured on `MappingRedisConverter`. - -The following example shows how to define a type alias for an entity: - -.Defining `@TypeAlias` for an entity -==== -[source,java] ----- -@TypeAlias("pers") -class Person { - -} ----- -==== - -The resulting document contains `pers` as the value in a `_class` field. - -[[configuring-custom-type-mapping]] -=== Configuring Custom Type Mapping - -The following example demonstrates how to configure a custom `RedisTypeMapper` in `MappingRedisConverter`: - -.Configuring a custom `RedisTypeMapper` via Spring Java Config -==== -[source,java] ----- -class CustomRedisTypeMapper extends DefaultRedisTypeMapper { - //implement custom type mapping here -} ----- - -[source,java] ----- -@Configuration -class SampleRedisConfiguration { - - @Bean - public MappingRedisConverter redisConverter(RedisMappingContext mappingContext, - RedisCustomConversions customConversions, ReferenceResolver referenceResolver) { - - MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver, - customTypeMapper()); - - mappingRedisConverter.setCustomConversions(customConversions); - - return mappingRedisConverter; - } - - @Bean - public RedisTypeMapper customTypeMapper() { - return new CustomRedisTypeMapper(); - } -} ----- -==== - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/queries.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/queries.adoc deleted file mode 100644 index 0bdcbee0b..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/queries.adoc +++ /dev/null @@ -1,78 +0,0 @@ -[[redis.repositories.queries]] -= Redis-specific Query Methods - -Query methods allow automatic derivation of simple finder queries from the method name, as shown in the following example: - -.Sample Repository finder Method -==== -[source,java] ----- -public interface PersonRepository extends CrudRepository { - - List findByFirstname(String firstname); -} ----- -==== - -NOTE: Please make sure properties used in finder methods are set up for indexing. - -NOTE: Query methods for Redis repositories support only queries for entities and collections of entities with paging. - -Using derived query methods might not always be sufficient to model the queries to run. `RedisCallback` offers more control over the actual matching of index structures or even custom indexes. -To do so, provide a `RedisCallback` that returns a single or `Iterable` set of `id` values, as shown in the following example: - -.Sample finder using RedisCallback -==== -[source,java] ----- -String user = //... - -List sessionsByUser = template.find(new RedisCallback>() { - - public Set doInRedis(RedisConnection connection) throws DataAccessException { - return connection - .sMembers("sessions:securityContext.authentication.principal.username:" + user); - }}, RedisSession.class); ----- -==== - -The following table provides an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to: - -==== -.Supported keywords inside method names -[options = "header, autowidth"] -|=============== -|Keyword|Sample|Redis snippet -|`And`|`findByLastnameAndFirstname`|`SINTER …:firstname:rand …:lastname:al’thor` -|`Or`|`findByLastnameOrFirstname`|`SUNION …:firstname:rand …:lastname:al’thor` -|`Is, Equals`|`findByFirstname`, `findByFirstnameIs`, `findByFirstnameEquals`|`SINTER …:firstname:rand` -|`IsTrue` | `FindByAliveIsTrue` | `SINTER …:alive:1` -|`IsFalse` | `findByAliveIsFalse` | `SINTER …:alive:0` -|`Top,First`|`findFirst10ByFirstname`,`findTop5ByFirstname`| -|=============== -==== - -[[redis.repositories.queries.sort]] -== Sorting Query Method results - -Redis repositories allow various approaches to define sorting order. -Redis itself does not support in-flight sorting when retrieving hashes or sets. -Therefore, Redis repository query methods construct a `Comparator` that is applied to the result before returning results as `List`. -Let's take a look at the following example: - -.Sorting Query Results -==== -[source,java] ----- -interface PersonRepository extends RedisRepository { - - List findByFirstnameOrderByAgeDesc(String firstname); <1> - - List findByFirstname(String firstname, Sort sort); <2> -} ----- - -<1> Static sorting derived from method name. -<2> Dynamic sorting using a method argument. -==== - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/query-by-example.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/query-by-example.adoc deleted file mode 100644 index e12b6eb6d..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/query-by-example.adoc +++ /dev/null @@ -1,46 +0,0 @@ -include::{commons}@data-commons::page$query-by-example.adoc[] - -[[query-by-example.running]] -== Running an Example - -The following example uses Query by Example against a repository: - -.Query by Example using a Repository -==== -[source, java] ----- -interface PersonRepository extends ListQueryByExampleExecutor { -} - -class PersonService { - - @Autowired PersonRepository personRepository; - - List findPeople(Person probe) { - return personRepository.findAll(Example.of(probe)); - } -} ----- -==== - -Redis Repositories support, with their secondary indexes, a subset of Spring Data's Query by Example features. -In particular, only exact, case-sensitive, and non-null values are used to construct a query. - -Secondary indexes use set-based operations (Set intersection, Set union) to determine matching keys. Adding a property to the query that is not indexed returns no result, because no index exists. Query by Example support inspects indexing configuration to include only properties in the query that are covered by an index. This is to prevent accidental inclusion of non-indexed properties. - -Case-insensitive queries and unsupported `StringMatcher` instances are rejected at runtime. - -The following list shows the supported Query by Example options: - -* Case-sensitive, exact matching of simple and nested properties -* Any/All match modes -* Value transformation of the criteria value -* Exclusion of `null` values from the criteria - -The following list shows properties not supported by Query by Example: - -* Case-insensitive matching -* Regex, prefix/contains/suffix String-matching -* Querying of Associations, Collection, and Map-like properties -* Inclusion of `null` values from the criteria -* `findAll` with sorting diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc deleted file mode 100644 index e7f4fae34..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-repositories/usage.adoc +++ /dev/null @@ -1,161 +0,0 @@ -[[redis.repositories.usage]] -= Usage - -Spring Data Redis lets you easily implement domain entities, as shown in the following example: - -.Sample Person Entity -==== -[source,java] ----- -@RedisHash("people") -public class Person { - - @Id String id; - String firstname; - String lastname; - Address address; -} ----- -==== - -We have a pretty simple domain object here. -Note that it has a `@RedisHash` annotation on its type and a property named `id` that is annotated with `org.springframework.data.annotation.Id`. -Those two items are responsible for creating the actual key used to persist the hash. - -NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. -Those with the annotation are favored over others. - -To now actually have a component responsible for storage and retrieval, we need to define a repository interface, as shown in the following example: - -.Basic Repository Interface To Persist Person Entities -==== -[source,java] ----- -public interface PersonRepository extends CrudRepository { - -} ----- -==== - -As our repository extends `CrudRepository`, it provides basic CRUD and finder operations. -The thing we need in between to glue things together is the corresponding Spring configuration, shown in the following example: - -.JavaConfig for Redis Repositories -==== -[source,java] ----- -@Configuration -@EnableRedisRepositories -public class ApplicationConfig { - - @Bean - public RedisConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - return template; - } -} ----- -==== - -Given the preceding setup, we can inject `PersonRepository` into our components, as shown in the following example: - -.Access to Person Entities -==== -[source,java] ----- -@Autowired PersonRepository repo; - -public void basicCrudOperations() { - - Person rand = new Person("rand", "al'thor"); - rand.setAddress(new Address("emond's field", "andor")); - - repo.save(rand); <1> - - repo.findOne(rand.getId()); <2> - - repo.count(); <3> - - repo.delete(rand); <4> -} ----- - -<1> Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Redis Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2025-beeb-c666fab4c0e5`. -<2> Uses the provided `id` to retrieve the object stored at `keyspace:id`. -<3> Counts the total number of entities available within the keyspace, `people`, defined by `@RedisHash` on `Person`. -<4> Removes the key for the given object from Redis. -==== - -[[redis.repositories.references]] -== Persisting References - -Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself. -On loading from Redis, references are resolved automatically and mapped back into the object, as shown in the following example: - -.Sample Property Reference -==== -[source,text] ----- -_class = org.example.Person -id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand -lastname = al’thor -mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1> ----- - -<1> Reference stores the whole key (`keyspace:id`) of the referenced object. -==== - -WARNING: Referenced Objects are not persisted when the referencing object is saved. -You must persist changes on referenced objects separately, since only the reference is stored. -Indexes set on properties of referenced types are not resolved. - -[[redis.repositories.partial-updates]] -== Persisting Partial Updates - -In some cases, you need not load and rewrite the entire entity just to set a new value within it. -A session timestamp for the last active time might be such a scenario where you want to alter one property. -`PartialUpdate` lets you define `set` and `delete` actions on existing objects while taking care of updating potential expiration times of both the entity itself and index structures. -The following example shows a partial update: - -.Sample Partial Update -==== -[source,java] ----- -PartialUpdate update = new PartialUpdate("e2c7dcee", Person.class) - .set("firstname", "mat") <1> - .set("address.city", "emond's field") <2> - .del("age"); <3> - -template.update(update); - -update = new PartialUpdate("e2c7dcee", Person.class) - .set("address", new Address("caemlyn", "andor")) <4> - .set("attributes", singletonMap("eye-color", "grey")); <5> - -template.update(update); - -update = new PartialUpdate("e2c7dcee", Person.class) - .refreshTtl(true); <6> - .set("expiration", 1000); - -template.update(update); ----- - -<1> Set the simple `firstname` property to `mat`. -<2> Set the simple 'address.city' property to 'emond's field' without having to pass in the entire object. -This does not work when a custom conversion is registered. -<3> Remove the `age` property. -<4> Set complex `address` property. -<5> Set a map of values, which removes the previously existing map and replaces the values with the given ones. -<6> Automatically update the server expiration time when altering xref:redis/redis-repositories/expirations.adoc[Time To Live]. -==== - -NOTE: Updating complex objects as well as map (or other collection) structures requires further interaction with Redis to determine existing values, which means that rewriting the entire entity might be faster. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc deleted file mode 100644 index 25d916f63..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc +++ /dev/null @@ -1,332 +0,0 @@ -[[redis.streams]] -= Redis Streams - -Redis Streams model a log data structure in an abstract approach. Typically, logs are append-only data structures and are consumed from the beginning on, at a random position, or by streaming new messages. - -NOTE: Learn more about Redis Streams in the https://redis.io/topics/streams-intro[Redis reference documentation]. - -Redis Streams can be roughly divided into two areas of functionality: - -* Appending records -* Consuming records - -Although this pattern has similarities to xref:redis/pubsub.adoc[Pub/Sub], the main difference lies in the persistence of messages and how they are consumed. - -While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don't listen, you miss a message), Redis Stream use a persistent, append-only data type that retains messages until the stream is trimmed. Another difference in consumption is that Pub/Sub registers a server-side subscription. Redis pushes arriving messages to the client while Redis Streams require active polling. - -The `org.springframework.data.redis.connection` and `org.springframework.data.redis.stream` packages provide the core functionality for Redis Streams. - -[[redis.streams.send]] -== Appending - -To send a record, you can use, as with the other operations, either the low-level `RedisConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `RedisConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as records, as shown in the following example: - -[source,java] ----- -// append message through connection -RedisConnection con = … -byte[] stream = … -ByteRecord record = StreamRecords.rawBytes(…).withStreamKey(stream); -con.xAdd(record); - -// append message through RedisTemplate -RedisTemplate template = … -StringRecord record = StreamRecords.string(…).withStreamKey("my-stream"); -template.opsForStream().add(record); ----- - -Stream records carry a `Map`, key-value tuples, as their payload. Appending a record to a stream returns the `RecordId` that can be used as further reference. - -[[redis.streams.receive]] -== Consuming - -On the consuming side, one can consume one or multiple streams. Redis Streams provide read commands that allow consumption of the stream from an arbitrary position (random access) within the known stream content and beyond the stream end to consume new stream record. - -At the low-level, `RedisConnection` offers the `xRead` and `xReadGroup` methods that map the Redis commands for reading and reading within a consumer group, respectively. Note that multiple streams can be used as arguments. - -NOTE: Subscription commands in Redis can be blocking. That is, calling `xRead` on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the read command times out or receives a message. - -To consume stream messages, one can either poll for messages in application code, or use one of the two xref:redis/redis-streams.adoc#redis.streams.receive.containers[Asynchronous reception through Message Listener Containers], the imperative or the reactive one. Each time a new records arrives, the container notifies the application code. - -[[redis.streams.receive.synchronous]] -=== Synchronous reception - -While stream consumption is typically associated with asynchronous processing, it is possible to consume messages synchronously. The overloaded `StreamOperations.read(…)` methods provide this functionality. During a synchronous receive, the calling thread potentially blocks until a message becomes available. The property `StreamReadOptions.block` specifies how long the receiver should wait before giving up waiting for a message. - -[source,java] ----- -// Read message through RedisTemplate -RedisTemplate template = … - -List> messages = template.opsForStream().read(StreamReadOptions.empty().count(2), - StreamOffset.latest("my-stream")); - -List> messages = template.opsForStream().read(Consumer.from("my-group", "my-consumer"), - StreamReadOptions.empty().count(2), - StreamOffset.create("my-stream", ReadOffset.lastConsumed())) ----- - -[[redis.streams.receive.containers]] -=== Asynchronous reception through Message Listener Containers - -Due to its blocking nature, low-level polling is not attractive, as it requires connection and thread management for every single consumer. To alleviate this problem, Spring Data offers message listeners, which do all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). - -Spring Data ships with two implementations tailored to the used programming model: - -* javadoc:org.springframework.data.redis.stream.StreamMessageListenerContainer[] acts as message listener container for imperative programming models. It is used to consume records from a Redis Stream and drive the javadoc:org.springframework.data.redis.stream.StreamListener[] instances that are injected into it. -* javadoc:org.springframework.data.redis.stream.StreamReceiver[] provides a reactive variant of a message listener. It is used to consume messages from a Redis Stream as potentially infinite stream and emit stream messages through a `Flux`. - -`StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. - -Both containers allow runtime configuration changes so that you can add or remove subscriptions while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, it automatically performs a cleanup, and the thread is released. - - -[[imperative-streammessagelistenercontainer]] -==== Imperative `StreamMessageListenerContainer` - -In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the javadoc:org.springframework.data.redis.stream.StreamListener[] interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. - -[source,java] ----- -class ExampleStreamListener implements StreamListener> { - - @Override - public void onMessage(MapRecord message) { - - System.out.println("MessageId: " + message.getId()); - System.out.println("Stream: " + message.getStream()); - System.out.println("Body: " + message.getValue()); - } -} ----- - -`StreamListener` represents a functional interface so implementations can be rewritten using their Lambda form: - -[source,java] ----- -message -> { - - System.out.println("MessageId: " + message.getId()); - System.out.println("Stream: " + message.getStream()); - System.out.println("Body: " + message.getValue()); -}; ----- - -Once you’ve implemented your `StreamListener`, it’s time to create a message listener container and register a subscription: - -[source,java] ----- -RedisConnectionFactory connectionFactory = … -StreamListener> streamListener = … - -StreamMessageListenerContainerOptions> containerOptions = StreamMessageListenerContainerOptions - .builder().pollTimeout(Duration.ofMillis(100)).build(); - -StreamMessageListenerContainer> container = StreamMessageListenerContainer.create(connectionFactory, - containerOptions); - -Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), streamListener); ----- - -Please refer to the Javadoc of the various message listener containers for a full description of the features supported by each implementation. - -[[reactive-streamreceiver]] -==== Reactive `StreamReceiver` - -Reactive consumption of streaming data sources typically happens through a `Flux` of events or messages. The reactive receiver implementation is provided with `StreamReceiver` and its overloaded `receive(…)` messages. The reactive approach requires fewer infrastructure resources such as threads in comparison to `StreamMessageListenerContainer` as it is leveraging threading resources provided by the driver. The receiving stream is a demand-driven publisher of ``StreamMessage``: - -[source,java] ----- -Flux> messages = … - -return messages.doOnNext(it -> { - System.out.println("MessageId: " + message.getId()); - System.out.println("Stream: " + message.getStream()); - System.out.println("Body: " + message.getValue()); -}); ----- - -Now we need to create the `StreamReceiver` and register a subscription to consume stream messages: - -[source,java] ----- -ReactiveRedisConnectionFactory connectionFactory = … - -StreamReceiverOptions> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100)) - .build(); -StreamReceiver> receiver = StreamReceiver.create(connectionFactory, options); - -Flux> messages = receiver.receive(StreamOffset.fromStart("my-stream")); ----- - -Please refer to the Javadoc of the various message listener containers for a full description of the features supported by each implementation. - -NOTE: Demand-driven consumption uses backpressure signals to activate and deactivate polling. `StreamReceiver` subscriptions pause polling if the demand is satisfied until subscribers signal further demand. Depending on the `ReadOffset` strategy, this can cause messages to be skipped. - -[[redis.streams.acknowledge]] -=== `Acknowledge` strategies - -When you read with messages via a `Consumer Group`, the server will remember that a given message was delivered and add it to the Pending Entries List (PEL). A list of messages delivered but not yet acknowledged. + -Messages have to be acknowledged via `StreamOperations.acknowledge` in order to be removed from the Pending Entries List as shown in the snippet below. - -==== -[source,java] ----- -StreamMessageListenerContainer> container = ... - -container.receive(Consumer.from("my-group", "my-consumer"), <1> - StreamOffset.create("my-stream", ReadOffset.lastConsumed()), - msg -> { - - // ... - redisTemplate.opsForStream().acknowledge("my-group", msg); <2> - }); ----- -<1> Read as _my-consumer_ from group _my-group_. Received messages are not acknowledged. -<2> Acknowledged the message after processing. -==== - -TIP: To auto acknowledge messages on receive use `receiveAutoAck` instead of `receive`. - -[[redis.streams.receive.readoffset]] -=== `ReadOffset` strategies - -Stream read operations accept a read offset specification to consume messages from the given offset on. `ReadOffset` represents the read offset specification. Redis supports 3 variants of offsets, depending on whether you consume the stream standalone or within a consumer group: - -* `ReadOffset.latest()` – Read the latest message. -* `ReadOffset.from(…)` – Read after a specific message Id. -* `ReadOffset.lastConsumed()` – Read after the last consumed message Id (consumer-group only). - -In the context of a message container-based consumption, we need to advance (or increment) the read offset when consuming a message. Advancing depends on the requested `ReadOffset` and consumption mode (with/without consumer groups). The following matrix explains how containers advance `ReadOffset`: - -.ReadOffset Advancing -[options="header,footer,autowidth"] -|=== -| Read offset | Standalone | Consumer Group -| Latest | Read latest message | Read latest message -| Specific Message Id | Use last seen message as the next MessageId | Use last seen message as the next MessageId -| Last Consumed | Use last seen message as the next MessageId | Last consumed message as per consumer group -|=== - -Reading from a specific message id and the last consumed message can be considered safe operations that ensure consumption of all messages that were appended to the stream. -Using the latest message for read can skip messages that were added to the stream while the poll operation was in the state of dead time. Polling introduces a dead time in which messages can arrive between individual polling commands. Stream consumption is not a linear contiguous read but split into repeating `XREAD` calls. - -[[redis.streams.receive.serialization]] -== Serialization - -Any Record sent to the stream needs to be serialized to its binary format. Due to the streams closeness to the hash data structure the stream key, field names and values use the according serializers configured on the `RedisTemplate`. - -.Stream Serialization -[options="header,footer,autowidth"] -|=== -| Stream Property | Serializer | Description -| key | keySerializer | used for `Record#getStream()` -| field | hashKeySerializer | used for each map key in the payload -| value | hashValueSerializer | used for each map value in the payload -|=== - -Please make sure to review ``RedisSerializer``s in use and note that if you decide to not use any serializer you need to make sure those values are binary already. - -[[redis.streams.hashing]] -== Object Mapping - -[[simple-values]] -=== Simple Values - -`StreamOperations` allows to append simple values, via `ObjectRecord`, directly to the stream without having to put those values into a `Map` structure. -The value will then be assigned to an _payload_ field and can be extracted when reading back the value. - -[source,java] ----- -ObjectRecord record = StreamRecords.newRecord() - .in("my-stream") - .ofObject("my-value"); - -redisTemplate() - .opsForStream() - .add(record); <1> - -List> records = redisTemplate() - .opsForStream() - .read(String.class, StreamOffset.fromStart("my-stream")); ----- -<1> XADD my-stream * "_class" "java.lang.String" "_raw" "my-value" - -``ObjectRecord``s pass through the very same serialization process as the all other records, thus the Record can also obtained using the untyped read operation returning a `MapRecord`. - -[[complex-values]] -=== Complex Values - -Adding a complex value to the stream can be done in 3 ways: - -* Convert to simple value using e. g. a String JSON representation. -* Serialize the value with a suitable `RedisSerializer`. -* Convert the value into a `Map` suitable for serialization using a xref:redis/hash-mappers.adoc[`HashMapper`]. - -The first variant is the most straight forward one but neglects the field value capabilities offered by the stream structure, still the values in the stream will be readable for other consumers. -The 2nd option holds the same benefits as the first one, but may lead to a very specific consumer limitations as the all consumers must implement the very same serialization mechanism. -The `HashMapper` approach is the a bit more complex one making use of the steams hash structure, but flattening the source. Still other consumers remain able to read the records as long as suitable serializer combinations are chosen. - -NOTE: xref:redis/hash-mappers.adoc[HashMappers] convert the payload to a `Map` with specific types. Make sure to use Hash-Key and Hash-Value serializers that are capable of (de-)serializing the hash. - -[source,java] ----- -ObjectRecord record = StreamRecords.newRecord() - .in("user-logon") - .ofObject(new User("night", "angel")); - -redisTemplate() - .opsForStream() - .add(record); <1> - -List> records = redisTemplate() - .opsForStream() - .read(User.class, StreamOffset.fromStart("user-logon")); ----- -<1> XADD user-logon * "_class" "com.example.User" "firstname" "night" "lastname" "angel" - -`StreamOperations` use by default xref:redis/redis-repositories/mapping.adoc[ObjectHashMapper]. -You may provide a `HashMapper` suitable for your requirements when obtaining `StreamOperations`. - -[source,java] ----- -redisTemplate() - .opsForStream(new Jackson2HashMapper(true)) - .add(record); <1> ----- -<1> XADD user-logon * "firstname" "night" "@class" "com.example.User" "lastname" "angel" - -[NOTE] -==== -A `StreamMessageListenerContainer` may not be aware of any `@TypeAlias` used on domain types as those need to be resolved through a `MappingContext`. -Make sure to initialize `RedisMappingContext` with a `initialEntitySet`. - -[source,java] ----- -@Bean -RedisMappingContext redisMappingContext() { - RedisMappingContext ctx = new RedisMappingContext(); - ctx.setInitialEntitySet(Collections.singleton(Person.class)); - return ctx; -} - -@Bean -RedisConverter redisConverter(RedisMappingContext mappingContext) { - return new MappingRedisConverter(mappingContext); -} - -@Bean -ObjectHashMapper hashMapper(RedisConverter converter) { - return new ObjectHashMapper(converter); -} - -@Bean -StreamMessageListenerContainer streamMessageListenerContainer(RedisConnectionFactory connectionFactory, ObjectHashMapper hashMapper) { - StreamMessageListenerContainerOptions> options = StreamMessageListenerContainerOptions.builder() - .objectMapper(hashMapper) - .build(); - - return StreamMessageListenerContainer.create(connectionFactory, options); -} ----- -==== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/scripting.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/scripting.adoc deleted file mode 100644 index 42411fbd9..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/scripting.adoc +++ /dev/null @@ -1,78 +0,0 @@ -[[scripting]] -= Scripting - -Redis versions 2.6 and higher provide support for running Lua scripts through the https://redis.io/commands/eval[eval] and https://redis.io/commands/evalsha[evalsha] commands. Spring Data Redis provides a high-level abstraction for running scripts that handles serialization and automatically uses the Redis script cache. - -Scripts can be run by calling the `execute` methods of `RedisTemplate` and `ReactiveRedisTemplate`. Both use a configurable javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] (or javadoc:org.springframework.data.redis.core.script.ReactiveScriptExecutor[]) to run the provided script. By default, the javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] (or javadoc:org.springframework.data.redis.core.script.ReactiveScriptExecutor[]) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. - -The default javadoc:org.springframework.data.redis.core.script.ScriptExecutor[] optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Redis script cache. - -The following example runs a common "`check-and-set`" scenario by using a Lua script. This is an ideal use case for a Redis script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. - -[source,java] ----- -@Bean -public RedisScript script() { - - ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")); - return RedisScript.of(scriptSource, Boolean.class); -} ----- - -[tabs] -====== -Imperative:: -+ -[source,java,role="primary"] ----- -public class Example { - - @Autowired - RedisOperations redisOperations; - - @Autowired - RedisScript script; - - public boolean checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, List.of("key"), expectedValue, newValue); - } -} ----- - -Reactive:: -+ -[source,java,role="secondary"] ----- -public class Example { - - @Autowired - ReactiveRedisOperations redisOperations; - - @Autowired - RedisScript script; - - public Flux checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, List.of("key"), expectedValue, newValue); - } -} ----- -====== - -[source,lua] ----- --- checkandset.lua -local current = redis.call('GET', KEYS[1]) -if current == ARGV[1] - then redis.call('SET', KEYS[1], ARGV[2]) - return true -end -return false ----- - -The preceding code configures a javadoc:org.springframework.data.redis.core.script.RedisScript[] pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). - -TIP: It is ideal to configure a single instance of `DefaultRedisScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. - -The `checkAndSet` method above then runs the scripts. Scripts can be run within a javadoc:org.springframework.data.redis.core.SessionCallback[] as part of a transaction or pipeline. See "`xref:redis/transactions.adoc[Redis Transactions]`" and "`xref:redis/pipelining.adoc[Pipelining]`" for more information. - -The scripting support provided by Spring Data Redis also lets you schedule Redis scripts for periodic running by using the Spring Task and Scheduler abstractions. See the https://spring.io/projects/spring-framework/[Spring Framework] documentation for more details. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc deleted file mode 100644 index e363bb4f5..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/support-classes.adoc +++ /dev/null @@ -1,67 +0,0 @@ -[[redis:support]] -= Support Classes - -Package `org.springframework.data.redis.support` offers various reusable components that rely on Redis as a backing store. -Currently, the package contains various JDK-based interface implementations on top of Redis, such as https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html[atomic] counters and JDK https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html[Collections]. - -NOTE: javadoc:org.springframework.data.redis.support.collections.RedisList[] is forward-compatible with Java 21 `SequencedCollection`. - -The atomic counters make it easy to wrap Redis key incrementation while the collections allow easy management of Redis keys with minimal storage exposure or API leakage. -In particular, the javadoc:org.springframework.data.redis.support.collections.RedisSet[] and javadoc:org.springframework.data.redis.support.collections.RedisZSet[] interfaces offer easy access to the set operations supported by Redis, such as `intersection` and `union`. javadoc:org.springframework.data.redis.support.collections.RedisList[] implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. -The following example shows the configuration for a bean that uses a javadoc:org.springframework.data.redis.support.collections.RedisList[]: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -class MyConfig { - - // … - - @Bean - RedisList stringRedisTemplate(RedisTemplate redisTemplate) { - return new DefaultRedisList<>(template, "queue-key"); - } -} ----- - -XML:: -+ -[source,xml,role="secondary"] ----- - - - - - - - - - ----- -====== - -The following example shows a Java configuration example for a `Deque`: - -[source,java] ----- -public class AnotherExample { - - // injected - private Deque queue; - - public void addTag(String tag) { - queue.push(tag); - } -} ----- - -As shown in the preceding example, the consuming code is decoupled from the actual storage implementation. -In fact, there is no indication that Redis is used underneath. -This makes moving from development to production environments transparent and highly increases testability (the Redis implementation can be replaced with an in-memory one). diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/template.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/template.adoc deleted file mode 100644 index 05af2e789..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/template.adoc +++ /dev/null @@ -1,386 +0,0 @@ -[[redis:template]] -= Working with Objects through `RedisTemplate` - -Most users are likely to use javadoc:org.springframework.data.redis.core.RedisTemplate[] and its corresponding package, `org.springframework.data.redis.core` or its reactive variant javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[]. -The template is, in fact, the central class of the Redis module, due to its rich feature set. -The template offers a high-level abstraction for Redis interactions. -While `[Reactive]RedisConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. - -The javadoc:org.springframework.data.redis.core.RedisTemplate[] class implements the javadoc:org.springframework.data.redis.core.RedisOperations[] interface and its reactive variant javadoc:org.springframework.data.redis.core.ReactiveRedisTemplate[] implements javadoc:org.springframework.data.redis.core.ReactiveRedisOperations[]. - -NOTE: The preferred way to reference operations on a `[Reactive]RedisTemplate` instance is through the -`[Reactive]RedisOperations` interface. - -Moreover, the template provides operations views (following the grouping from the Redis command https://redis.io/commands[reference]) that offer rich, generified interfaces for working against a certain type or certain key (through the `KeyBound` interfaces) as described in the following table: - -.Operational views -[%collapsible] -======= -[tabs] -====== -Imperative:: -+ -[width="80%",cols="<1,<2",options="header",role="primary"] -|==== -|Interface -|Description - -2+^|_Key Type Operations_ - -|javadoc:org.springframework.data.redis.core.GeoOperations[] -|Redis geospatial operations, such as `GEOADD`, `GEORADIUS`,... - -|javadoc:org.springframework.data.redis.core.HashOperations[] -|Redis hash operations - -|javadoc:org.springframework.data.redis.core.HyperLogLogOperations[] -|Redis HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... - -|javadoc:org.springframework.data.redis.core.ListOperations[] -|Redis list operations - -|javadoc:org.springframework.data.redis.core.SetOperations[] -|Redis set operations - -|javadoc:org.springframework.data.redis.core.ValueOperations[] -|Redis string (or value) operations - -|javadoc:org.springframework.data.redis.core.ZSetOperations[] -|Redis zset (or sorted set) operations - -2+^|_Key Bound Operations_ - -|javadoc:org.springframework.data.redis.core.BoundGeoOperations[] -|Redis key bound geospatial operations - -|javadoc:org.springframework.data.redis.core.BoundHashOperations[] -|Redis hash key bound operations - -|javadoc:org.springframework.data.redis.core.BoundKeyOperations[] -|Redis key bound operations - -|javadoc:org.springframework.data.redis.core.BoundListOperations[] -|Redis list key bound operations - -|javadoc:org.springframework.data.redis.core.BoundSetOperations[] -|Redis set key bound operations - -|javadoc:org.springframework.data.redis.core.BoundValueOperations[] -|Redis string (or value) key bound operations - -|javadoc:org.springframework.data.redis.core.BoundZSetOperations[] -|Redis zset (or sorted set) key bound operations - -|==== - -Reactive:: -+ -[width="80%",cols="<1,<2",options="header",role="secondary"] -|==== -|Interface -|Description - -2+^|_Key Type Operations_ - -|javadoc:org.springframework.data.redis.core.ReactiveGeoOperations[] -|Redis geospatial operations such as `GEOADD`, `GEORADIUS`, and others) - -|javadoc:org.springframework.data.redis.core.ReactiveHashOperations[] -|Redis hash operations - -|javadoc:org.springframework.data.redis.core.ReactiveHyperLogLogOperations[] -|Redis HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) - -|javadoc:org.springframework.data.redis.core.ReactiveListOperations[] -|Redis list operations - -|javadoc:org.springframework.data.redis.core.ReactiveSetOperations[] -|Redis set operations - -|javadoc:org.springframework.data.redis.core.ReactiveValueOperations[] -|Redis string (or value) operations - -|javadoc:org.springframework.data.redis.core.ReactiveZSetOperations[] -|Redis zset (or sorted set) operations -|==== -====== -======= - -Once configured, the template is thread-safe and can be reused across multiple instances. - -`RedisTemplate` uses a Java-based serializer for most of its operations. -This means that any object written or read by the template is serialized and deserialized through Java. - -You can change the serialization mechanism on the template, and the Redis module offers several implementations, which are available in the `org.springframework.data.redis.serializer` package. -See <> for more information. -You can also set any of the serializers to null and use RedisTemplate with raw byte arrays by setting the `enableDefaultSerializer` property to `false`. -Note that the template requires all keys to be non-null. -However, values can be null as long as the underlying serializer accepts them. -Read the Javadoc of each serializer for more information. - -For cases where you need a certain template view, declare the view as a dependency and inject the template. -The container automatically performs the conversion, eliminating the `opsFor[X]` calls, as shown in the following example: - -.Configuring Template API -[tabs] -====== -Java Imperative:: -+ -[source,java,role="primary"] ----- -@Configuration -class MyConfig { - - @Bean - LettuceConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - return template; - } -} ----- - -Java Reactive:: -+ -[source,java,role="secondary"] ----- -@Configuration -class MyConfig { - - @Bean - LettuceConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - ReactiveRedisTemplate ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) { - return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string()); - } -} ----- - -XML:: -+ -[source,xml,role="tertiary"] ----- - - - - - - - ... - - ----- -====== - -.Pushing an item to a List using `[Reactive]RedisTemplate` -[tabs] -====== -Imperative:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- -public class Example { - - // inject the actual operations - @Autowired - private RedisOperations operations; - - // inject the template as ListOperations - @Resource(name="redisTemplate") - private ListOperations listOps; - - public void addLink(String userId, URL url) { - listOps.leftPush(userId, url.toExternalForm()); - } -} ----- - -Reactive:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="secondary"] ----- -public class Example { - - // inject the actual template - @Autowired - private ReactiveRedisOperations operations; - - public Mono addLink(String userId, URL url) { - return operations.opsForList().leftPush(userId, url.toExternalForm()); - } -} ----- -====== - -[[redis:string]] -== String-focused Convenience Classes - -Since it is quite common for the keys and values stored in Redis to be `java.lang.String`, the Redis modules provides two extensions to `RedisConnection` and `RedisTemplate`, respectively the `StringRedisConnection` (and its `DefaultStringRedisConnection` implementation) and `StringRedisTemplate` as a convenient one-stop solution for intensive String operations. -In addition to being bound to `String` keys, the template and the connection use the `StringRedisSerializer` underneath, which means the stored keys and values are human-readable (assuming the same encoding is used both in Redis and your code). -The following listings show an example: - -[tabs] -====== -Java Imperative:: -+ -[source,java,role="primary"] ----- -@Configuration -class RedisConfiguration { - - @Bean - LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - - StringRedisTemplate template = new StringRedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - return template; - } -} ----- - -Java Reactive:: -+ -[source,java,role="secondary"] ----- -@Configuration -class RedisConfiguration { - - @Bean - LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - ReactiveStringRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) { - return new ReactiveStringRedisTemplate<>(factory); - } -} ----- - -XML:: -+ -[source,xml,role="tertiary"] ----- - - - - - - - - ----- -====== - -[tabs] -====== -Imperative:: -+ -[source,java,role="primary"] ----- -public class Example { - - @Autowired - private StringRedisTemplate redisTemplate; - - public void addLink(String userId, URL url) { - redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); - } -} ----- - -Reactive:: -+ -[source,java,role="secondary"] ----- -public class Example { - - @Autowired - private ReactiveStringRedisTemplate redisTemplate; - - public Mono addLink(String userId, URL url) { - return redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); - } -} ----- -====== - -As with the other Spring templates, `RedisTemplate` and `StringRedisTemplate` let you talk directly to Redis through the `RedisCallback` interface. -This feature gives complete control to you, as it talks directly to the `RedisConnection`. -Note that the callback receives an instance of `StringRedisConnection` when a `StringRedisTemplate` is used. -The following example shows how to use the `RedisCallback` interface: - -[source,java] ----- -public void useCallback() { - - redisOperations.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - Long size = connection.dbSize(); - // Can cast to StringRedisConnection if using a StringRedisTemplate - ((StringRedisConnection)connection).set("key", "value"); - } - }); -} ----- - -[[redis:serializer]] -== Serializers - -From the framework perspective, the data stored in Redis is only bytes. -While Redis itself supports various types, for the most part, these refer to the way the data is stored rather than what it represents. -It is up to the user to decide whether the information gets translated into strings or any other objects. - -In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Redis in the `org.springframework.data.redis.serializer` package. - -This package contains two types of serializers that, as the name implies, take care of the serialization process: - -* Two-way serializers based on javadoc:org.springframework.data.redis.serializer.RedisSerializer[]. -* Element readers and writers that use `RedisElementReader` and ``RedisElementWriter``. - -The main difference between these variants is that `RedisSerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. - -Multiple implementations are available (including two that have been already mentioned in this documentation): - -* javadoc:org.springframework.data.redis.serializer.JdkSerializationRedisSerializer[], which is used by default for javadoc:org.springframework.data.redis.cache.RedisCache[] and javadoc:org.springframework.data.redis.core.RedisTemplate[]. -* the `StringRedisSerializer`. - -However, one can use `OxmSerializer` for Object/XML mapping through Spring {spring-framework-docs}/data-access.html#oxm[OXM] support or javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] or javadoc:org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer[] for storing data in https://en.wikipedia.org/wiki/JSON[JSON] format. - -Do note that the storage format is not limited only to values. -It can be used for keys, values, or hashes without any restrictions. - -[WARNING] -==== -By default, javadoc:org.springframework.data.redis.cache.RedisCache[] and javadoc:org.springframework.data.redis.core.RedisTemplate[] are configured to use Java native serialization. -Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. -Manipulated input could lead to unwanted code being run in the application during the deserialization step. -As a consequence, do not use serialization in untrusted environments. -In general, we strongly recommend any other message format (such as JSON) instead. - -If you are concerned about security vulnerabilities due to Java serialization, consider the general-purpose serialization filter mechanism at the core JVM level: - -* https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html[Filter Incoming Serialization Data]. -* https://openjdk.org/jeps/290[JEP 290]. -* https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data[OWASP: Deserialization of untrusted data]. -==== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/transactions.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/transactions.adoc deleted file mode 100644 index 8ba20676b..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/redis/transactions.adoc +++ /dev/null @@ -1,120 +0,0 @@ -[[tx]] -= Redis Transactions - -Redis provides support for https://redis.io/topics/transactions[transactions] through the `multi`, `exec`, and `discard` commands. -These operations are available on javadoc:org.springframework.data.redis.core.RedisTemplate[]. -However, `RedisTemplate` is not guaranteed to run all the operations in the transaction with the same connection. - -Spring Data Redis provides the javadoc:org.springframework.data.redis.core.SessionCallback[] interface for use when multiple operations need to be performed with the same `connection`, such as when using Redis transactions.The following example uses the `multi` method: - -[source,java] ----- -//execute a transaction -List txResults = redisOperations.execute(new SessionCallback>() { - public List execute(RedisOperations operations) throws DataAccessException { - operations.multi(); - operations.opsForSet().add("key", "value1"); - - // This will contain the results of all operations in the transaction - return operations.exec(); - } -}); -System.out.println("Number of items added to set: " + txResults.get(0)); ----- - -`RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results of `exec` before returning. -There is an additional `exec` method that lets you pass a custom serializer for transaction results. - -It is worth mentioning that in case between `multi()` and `exec()` an exception happens (e.g. a timeout exception in case Redis does not respond within the timeout) then the connection may get stuck in a transactional state. -To prevent such a situation need have to discard the transactional state to clear the connection: - -[source,java] ----- -List txResults = redisOperations.execute(new SessionCallback>() { - public List execute(RedisOperations operations) throws DataAccessException { - boolean transactionStateIsActive = true; - try { - operations.multi(); - operations.opsForSet().add("key", "value1"); - - // This will contain the results of all operations in the transaction - return operations.exec(); - } catch (RuntimeException e) { - operations.discard(); - throw e; - } - } -}); ----- - -[[tx.spring]] -== `@Transactional` Support - -By default, `RedisTemplate` does not participate in managed Spring transactions. -If you want `RedisTemplate` to make use of Redis transaction when using `@Transactional` or `TransactionTemplate`, you need to be explicitly enable transaction support for each `RedisTemplate` by setting `setEnableTransactionSupport(true)`. -Enabling transaction support binds `RedisConnection` to the current transaction backed by a `ThreadLocal`. -If the transaction finishes without errors, the Redis transaction gets commited with `EXEC`, otherwise rolled back with `DISCARD`. -Redis transactions are batch-oriented. -Commands issued during an ongoing transaction are queued and only applied when committing the transaction. - -Spring Data Redis distinguishes between read-only and write commands in an ongoing transaction. -Read-only commands, such as `KEYS`, are piped to a fresh (non-thread-bound) `RedisConnection` to allow reads. -Write commands are queued by `RedisTemplate` and applied upon commit. - -The following example shows how to configure transaction management: - -.Configuration enabling Transaction Management -==== -[source,java] ----- -@Configuration -@EnableTransactionManagement <1> -public class RedisTxContextConfiguration { - - @Bean - public StringRedisTemplate redisTemplate() { - StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory()); - // explicitly enable transaction support - template.setEnableTransactionSupport(true); <2> - return template; - } - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - // jedis || Lettuce - } - - @Bean - public PlatformTransactionManager transactionManager() throws SQLException { - return new DataSourceTransactionManager(dataSource()); <3> - } - - @Bean - public DataSource dataSource() throws SQLException { - // ... - } -} ----- -<1> Configures a Spring Context to enable {spring-framework-docs}/data-access.html#transaction-declarative[declarative transaction management]. -<2> Configures `RedisTemplate` to participate in transactions by binding connections to the current thread. -<3> Transaction management requires a `PlatformTransactionManager`. -Spring Data Redis does not ship with a `PlatformTransactionManager` implementation. -Assuming your application uses JDBC, Spring Data Redis can participate in transactions by using existing transaction managers. -==== - -The following examples each demonstrate a usage constraint: - -.Usage Constraints -==== -[source,java] ----- -// must be performed on thread-bound connection -template.opsForValue().set("thing1", "thing2"); - -// read operation must be run on a free (not transaction-aware) connection -template.keys("*"); - -// returns null as values set within a transaction are not visible -template.opsForValue().get("thing1"); ----- -==== diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories.adoc deleted file mode 100644 index cc338e30e..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories.adoc +++ /dev/null @@ -1,15 +0,0 @@ -[[redis.repositories]] -= Redis Repositories -:page-section-summary-toc: 1 - -This chapter explains the basic foundations of Spring Data repositories and Redis specifics. -Before continuing to the Redis specifics, make sure you have a sound understanding of the basic concepts. - -The goal of the Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores. - -Working with Redis Repositories lets you seamlessly convert and store domain objects in Redis Hashes, apply custom mapping strategies, and use secondary indexes. - -IMPORTANT: Redis Repositories require at least Redis Server version 2.8.0 and do not work with transactions. -Make sure to use a `RedisTemplate` with xref:redis/transactions.adoc#tx.spring[disabled transaction support]. - - diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc deleted file mode 100644 index b180e0222..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc +++ /dev/null @@ -1,4 +0,0 @@ -include::{commons}@data-commons::page$repositories/core-concepts.adoc[] - -[[redis.entity-persistence.state-detection-strategies]] -include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc deleted file mode 100644 index f84313e9d..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-domain-events.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/core-domain-events.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc deleted file mode 100644 index 359564aea..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[core.extensions.querydsl]] -= Querydsl - -Spring Data Redis does not support Querydsl. diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc deleted file mode 100644 index 2ae01801b..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/create-instances.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/create-instances.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc deleted file mode 100644 index c7615191a..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/custom-implementations.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/custom-implementations.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/definition.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/definition.adoc deleted file mode 100644 index bd65a8af8..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/definition.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/definition.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc deleted file mode 100644 index 081bac9f6..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/null-handling.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/object-mapping.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/object-mapping.adoc deleted file mode 100644 index 0b6fd54dd..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/object-mapping.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$object-mapping.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/projections.adoc deleted file mode 100644 index 1fae30fda..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[redis.projections]] -= Projections - -include::{commons}@data-commons::page$repositories/projections.adoc[leveloffset=+1] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc deleted file mode 100644 index e495eddc6..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-keywords-reference.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/query-keywords-reference.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc deleted file mode 100644 index dfe481495..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc deleted file mode 100644 index a73c3201d..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/repositories/query-return-types-reference.adoc +++ /dev/null @@ -1 +0,0 @@ -include::{commons}@data-commons::page$repositories/query-return-types-reference.adoc[] diff --git a/spring-data-valkey/src/main/antora/modules/ROOT/pages/upgrading.adoc b/spring-data-valkey/src/main/antora/modules/ROOT/pages/upgrading.adoc deleted file mode 100644 index e404cc556..000000000 --- a/spring-data-valkey/src/main/antora/modules/ROOT/pages/upgrading.adoc +++ /dev/null @@ -1,268 +0,0 @@ -[[redis.upgrading]] -= Migration Guides - -This section contains details about migration steps, deprecations, and removals. - -[[upgrading.2-to-3]] -== Upgrading from 2.x to 3.x - -[[upgrading.2-to-3.types]] -=== Re-/moved Types - -|=== -|Type |Replacement - -|o.s.d.redis.Version -|o.s.d.util.Version - -|o.s.d.redis.VersionParser -|- - -|o.s.d.redis.connection.RedisZSetCommands.Aggregate -|o.s.d.redis.connection.zset.Aggregate - -|o.s.d.redis.connection.RedisZSetCommands.Tuple -|o.s.d.redis.connection.zset.Tuple - -|o.s.d.redis.connection.RedisZSetCommands.Weights -|o.s.d.redis.connection.zset.Weights - -|o.s.d.redis.connection.RedisZSetCommands.Range -|o.s.d.domain.Range - -|o.s.d.redis.connection.RedisZSetCommands.Limit -|o.s.d.redis.connection.Limit.java - -|o.s.d.redis.connection.jedis.JedisUtils -|- - -|o.s.d.redis.connection.jedis.JedisVersionUtil -|- - -|o.s.d.redis.core.convert.CustomConversions -|o.s.d.convert.CustomConversions - -|=== - -[[changed-methods-and-types]] -=== Changed Methods and Types - -.Core -|=== -|Type |Method |Replacement - -|o.s.d.redis.core.Cursor -|open -|- - -|o.s.d.redis.core.RedisTemplate -|execute -|doWithKeys - -|o.s.d.redis.stream.StreamMessageListenerContainer -|isAutoAck -|isAutoAcknowledge - -|o.s.d.redis.stream.StreamMessageListenerContainer -|autoAck -|autoAcknowledge - -|=== - -.Redis Connection -|=== -|Type |Method |Replacement - -|o.s.d.redis.connection.ClusterCommandExecutionFailureException -|getCauses -|getSuppressed - -|o.s.d.redis.connection.RedisConnection -|bgWriteAof -|bgReWriteAof - -|o.s.d.redis.connection.RedisConnection -|slaveOf -|replicaOf - -|o.s.d.redis.connection.RedisConnection -|slaveOfNoOne -|replicaOfNoOne - -|o.s.d.redis.connection.ReactiveClusterCommands -|clusterGetSlaves -|clusterGetReplicas - -|o.s.d.redis.connection.ReactiveClusterCommands -|clusterGetMasterSlaveMap -|clusterGetMasterReplicaMap - -|o.s.d.redis.connection.ReactiveKeyCommands -|getNewName -|getNewKey - -|o.s.d.redis.connection.RedisClusterNode.Flag -|SLAVE -|REPLICA - -|o.s.d.redis.connection.RedisClusterNode.Builder -|slaveOf -|replicaOf - -|o.s.d.redis.connection.RedisNode -|isSlave -|isReplica - -|o.s.d.redis.connection.RedisSentinelCommands -|slaves -|replicas - -|o.s.d.redis.connection.RedisServer -|getNumberSlaves -|getNumberReplicas - -|o.s.d.redis.connection.RedisServerCommands -|slaveOf -|replicaOf - -|o.s.d.redis.core.ClusterOperations -|getSlaves -|getReplicas - -|o.s.d.redis.core.RedisOperations -|slaveOf -|replicaOf - -|=== - -.Redis Operations -|=== -|Type |Method |Replacement - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoAdd -|add - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoDist -|distance - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoHash -|hash - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoPos -|position - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoRadius -|radius - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoRadiusByMember -|radius - -|o.s.d.redis.core.GeoOperations & BoundGeoOperations -|geoRemove -|remove - -|=== - -.Redis Cache -|=== -|Type |Method |Replacement - -|o.s.d.redis.cache.RedisCacheConfiguration -|prefixKeysWith -|prefixCacheNameWith - -|o.s.d.redis.cache.RedisCacheConfiguration -|getKeyPrefix -|getKeyPrefixFor - -|=== - -[[upgrading.2-to-3.jedis]] -=== Jedis - -Please read the Jedis https://github.com/redis/jedis/blob/v4.0.0/docs/3to4.md[upgrading guide] which covers important driver changes. - -.Jedis Redis Connection -|=== -|Type |Method |Replacement - -|o.s.d.redis.connection.jedis.JedisConnectionFactory -|getShardInfo -|_can be obtained via JedisClientConfiguration_ - -|o.s.d.redis.connection.jedis.JedisConnectionFactory -|setShardInfo -|_can be set via JedisClientConfiguration_ - -|o.s.d.redis.connection.jedis.JedisConnectionFactory -|createCluster -|_now requires a `Connection` instead of `Jedis` instance_ - -|o.s.d.redis.connection.jedis.JedisConverters -| -|has package visibility now - -|o.s.d.redis.connection.jedis.JedisConverters -|tuplesToTuples -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|tuplesToTuples -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|stringListToByteList -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|stringSetToByteSet -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|stringMapToByteMap -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|tupleSetToTupleSet -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|toTupleSet -|- - -|o.s.d.redis.connection.jedis.JedisConverters -|toDataAccessException -|o.s.d.redis.connection.jedis.JedisExceptionConverter#convert - -|=== - -[[upgrading.2-to-3.jedis.transactions]] -=== Transactions / Pipelining - -Pipelining and Transactions are now mutually exclusive. -The usage of server or connection commands in pipeline/transactions mode is no longer possible. - -[[upgrading.2-to-3.lettuce]] -=== Lettuce - -[[upgrading.2-to-3.lettuce.pool]] -==== Lettuce Pool - -`LettucePool` and its implementation `DefaultLettucePool` have been removed without replacement. -Please refer to the https://lettuce.io/core/release/reference/index.html#_connection_pooling[driver documentation] for driver native pooling capabilities. -Methods accepting pooling parameters have been updated. -This effects methods on `LettuceConnectionFactory` and `LettuceConnection`. - -[[upgrading.2-to-3.lettuce.authentication]] -==== Lettuce Authentication - -`AuthenticatingRedisClient` has been removed without replacement. -Please refer to the https://lettuce.io/core/release/reference/index.html#basic.redisuri[driver documentation] for `RedisURI` to set authentication data. - - diff --git a/spring-data-valkey/src/main/antora/resources/antora-resources/antora.yml b/spring-data-valkey/src/main/antora/resources/antora-resources/antora.yml deleted file mode 100644 index 10dd13d03..000000000 --- a/spring-data-valkey/src/main/antora/resources/antora-resources/antora.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: ${antora-component.version} -prerelease: ${antora-component.prerelease} - -asciidoc: - attributes: - copyright-year: ${current.year} - version: ${project.version} - springversionshort: ${spring.short} - springversion: ${spring} - attribute-missing: 'warn' - commons: ${springdata.commons.docs} - lettuce: ${lettuce} - jedis: ${jedis} - include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference - spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ - springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api - spring-framework-docs: '{springdocsurl}' - spring-framework-javadoc: '{springjavadocurl}' - springhateoasversion: ${spring-hateoas} - releasetrainversion: ${releasetrain} - store: Redis From 1df2d44ffa21bec9571b3b35fc355b8f1dbeb23c Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Mon, 26 Jan 2026 14:59:26 -0800 Subject: [PATCH 03/21] Rebrand docs from Redis to Valkey Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 56 +-- docs/src/content/docs/appendix.md | 4 +- docs/src/content/docs/commons/migration.md | 110 +++--- docs/src/content/docs/index.mdx | 16 +- docs/src/content/docs/observability.md | 48 +-- docs/src/content/docs/overview.md | 10 +- docs/src/content/docs/preface.md | 26 +- docs/src/content/docs/redis.md | 42 --- .../content/docs/redis/connection-modes.md | 171 --------- .../content/docs/redis/getting-started.mdx | 86 ----- docs/src/content/docs/redis/pipelining.md | 47 --- docs/src/content/docs/redis/redis-cache.md | 269 -------------- .../redis-repositories/cdi-integration.md | 65 ---- docs/src/content/docs/redis/scripting.mdx | 82 ----- .../content/docs/redis/support-classes.mdx | 72 ---- docs/src/content/docs/redis/template.mdx | 341 ------------------ docs/src/content/docs/redis/transactions.md | 117 ------ docs/src/content/docs/repositories.md | 12 +- .../docs/repositories/core-extensions.md | 2 +- docs/src/content/docs/valkey.md | 42 +++ .../content/docs/{redis => valkey}/cluster.md | 54 +-- .../content/docs/valkey/connection-modes.md | 171 +++++++++ .../content/docs/{redis => valkey}/drivers.md | 68 ++-- .../content/docs/valkey/getting-started.mdx | 86 +++++ .../docs/{redis => valkey}/hash-mappers.md | 18 +- docs/src/content/docs/valkey/pipelining.md | 47 +++ .../content/docs/{redis => valkey}/pubsub.mdx | 84 ++--- docs/src/content/docs/valkey/scripting.mdx | 82 +++++ .../content/docs/valkey/support-classes.mdx | 72 ++++ docs/src/content/docs/valkey/template.mdx | 341 ++++++++++++++++++ docs/src/content/docs/valkey/transactions.md | 117 ++++++ docs/src/content/docs/valkey/valkey-cache.md | 269 ++++++++++++++ .../valkey-repositories}/anatomy.md | 6 +- .../valkey-repositories/cdi-integration.md | 65 ++++ .../valkey-repositories}/cluster.md | 10 +- .../valkey-repositories}/expirations.md | 30 +- .../valkey-repositories}/indexes.md | 28 +- .../valkey-repositories}/keyspaces.md | 20 +- .../valkey-repositories}/mapping.md | 44 +-- .../valkey-repositories}/queries.md | 28 +- .../valkey-repositories}/query-by-example.md | 4 +- .../valkey-repositories}/usage.md | 30 +- .../valkey-streams.md} | 88 ++--- 43 files changed, 1690 insertions(+), 1690 deletions(-) delete mode 100644 docs/src/content/docs/redis.md delete mode 100644 docs/src/content/docs/redis/connection-modes.md delete mode 100644 docs/src/content/docs/redis/getting-started.mdx delete mode 100644 docs/src/content/docs/redis/pipelining.md delete mode 100644 docs/src/content/docs/redis/redis-cache.md delete mode 100644 docs/src/content/docs/redis/redis-repositories/cdi-integration.md delete mode 100644 docs/src/content/docs/redis/scripting.mdx delete mode 100644 docs/src/content/docs/redis/support-classes.mdx delete mode 100644 docs/src/content/docs/redis/template.mdx delete mode 100644 docs/src/content/docs/redis/transactions.md create mode 100644 docs/src/content/docs/valkey.md rename docs/src/content/docs/{redis => valkey}/cluster.md (64%) create mode 100644 docs/src/content/docs/valkey/connection-modes.md rename docs/src/content/docs/{redis => valkey}/drivers.md (54%) create mode 100644 docs/src/content/docs/valkey/getting-started.mdx rename docs/src/content/docs/{redis => valkey}/hash-mappers.md (69%) create mode 100644 docs/src/content/docs/valkey/pipelining.md rename docs/src/content/docs/{redis => valkey}/pubsub.mdx (51%) create mode 100644 docs/src/content/docs/valkey/scripting.mdx create mode 100644 docs/src/content/docs/valkey/support-classes.mdx create mode 100644 docs/src/content/docs/valkey/template.mdx create mode 100644 docs/src/content/docs/valkey/transactions.md create mode 100644 docs/src/content/docs/valkey/valkey-cache.md rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/anatomy.md (95%) create mode 100644 docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/cluster.md (77%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/expirations.md (54%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/indexes.md (81%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/keyspaces.md (65%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/mapping.md (75%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/queries.md (59%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/query-by-example.md (97%) rename docs/src/content/docs/{redis/redis-repositories => valkey/valkey-repositories}/usage.md (79%) rename docs/src/content/docs/{redis/redis-streams.md => valkey/valkey-streams.md} (73%) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 16e8d628c..012870d12 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ site: 'https://spring.valkey.io', integrations: [ starlight({ - title: 'Spring Data Redis', + title: 'Spring Data Valkey', logo: { light: './src/assets/valkey-logo-with-name-light.svg', dark: './src/assets/valkey-logo-with-name-dark.svg', @@ -22,52 +22,52 @@ export default defineConfig({ { label: 'Overview', items: [ - { label: 'Spring Data Redis', slug: 'overview' }, + { label: 'Spring Data Valkey', slug: 'overview' }, { label: 'Upgrading Spring Data', slug: 'commons/upgrade' }, { label: 'Migration Guides', slug: 'commons/migration' }, ] }, { - label: 'Redis', + label: 'Valkey', items: [ - { label: 'Redis Overview', slug: 'redis' }, - { label: 'Getting Started', slug: 'redis/getting-started' }, - { label: 'Drivers', slug: 'redis/drivers' }, - { label: 'Connection Modes', slug: 'redis/connection-modes' }, - { label: 'RedisTemplate', slug: 'redis/template' }, - { label: 'Redis Cache', slug: 'redis/redis-cache' }, - { label: 'Cluster', slug: 'redis/cluster' }, - { label: 'Hash Mapping', slug: 'redis/hash-mappers' }, - { label: 'Pub/Sub Messaging', slug: 'redis/pubsub' }, - { label: 'Redis Streams', slug: 'redis/redis-streams' }, - { label: 'Scripting', slug: 'redis/scripting' }, - { label: 'Redis Transactions', slug: 'redis/transactions' }, - { label: 'Pipelining', slug: 'redis/pipelining' }, - { label: 'Support Classes', slug: 'redis/support-classes' }, + { label: 'Valkey Overview', slug: 'valkey' }, + { label: 'Getting Started', slug: 'valkey/getting-started' }, + { label: 'Drivers', slug: 'valkey/drivers' }, + { label: 'Connection Modes', slug: 'valkey/connection-modes' }, + { label: 'ValkeyTemplate', slug: 'valkey/template' }, + { label: 'Valkey Cache', slug: 'valkey/valkey-cache' }, + { label: 'Cluster', slug: 'valkey/cluster' }, + { label: 'Hash Mapping', slug: 'valkey/hash-mappers' }, + { label: 'Pub/Sub Messaging', slug: 'valkey/pubsub' }, + { label: 'Valkey Streams', slug: 'valkey/valkey-streams' }, + { label: 'Scripting', slug: 'valkey/scripting' }, + { label: 'Valkey Transactions', slug: 'valkey/transactions' }, + { label: 'Pipelining', slug: 'valkey/pipelining' }, + { label: 'Support Classes', slug: 'valkey/support-classes' }, ] }, { label: 'Repositories', items: [ - { label: 'Redis Repositories Overview', slug: 'repositories' }, + { label: 'Valkey Repositories Overview', slug: 'repositories' }, { label: 'Core concepts', slug: 'repositories/core-concepts' }, { label: 'Defining Repository Interfaces', slug: 'repositories/definition' }, { label: 'Creating Repository Instances', slug: 'repositories/create-instances' }, - { label: 'Usage', slug: 'redis/redis-repositories/usage' }, + { label: 'Usage', slug: 'valkey/valkey-repositories/usage' }, { label: 'Object Mapping Fundamentals', slug: 'repositories/object-mapping' }, - { label: 'Object-to-Hash Mapping', slug: 'redis/redis-repositories/mapping' }, - { label: 'Keyspaces', slug: 'redis/redis-repositories/keyspaces' }, - { label: 'Secondary Indexes', slug: 'redis/redis-repositories/indexes' }, - { label: 'Time To Live', slug: 'redis/redis-repositories/expirations' }, - { label: 'Redis-specific Query Methods', slug: 'redis/redis-repositories/queries' }, - { label: 'Query by Example', slug: 'redis/redis-repositories/query-by-example' }, - { label: 'Redis Repositories Running on a Cluster', slug: 'redis/redis-repositories/cluster' }, - { label: 'Redis Repositories Anatomy', slug: 'redis/redis-repositories/anatomy' }, + { label: 'Object-to-Hash Mapping', slug: 'valkey/valkey-repositories/mapping' }, + { label: 'Keyspaces', slug: 'valkey/valkey-repositories/keyspaces' }, + { label: 'Secondary Indexes', slug: 'valkey/valkey-repositories/indexes' }, + { label: 'Time To Live', slug: 'valkey/valkey-repositories/expirations' }, + { label: 'Valkey-specific Query Methods', slug: 'valkey/valkey-repositories/queries' }, + { label: 'Query by Example', slug: 'valkey/valkey-repositories/query-by-example' }, + { label: 'Valkey Repositories Running on a Cluster', slug: 'valkey/valkey-repositories/cluster' }, + { label: 'Valkey Repositories Anatomy', slug: 'valkey/valkey-repositories/anatomy' }, { label: 'Projections', slug: 'repositories/projections' }, { label: 'Custom Repository Implementations', slug: 'repositories/custom-implementations' }, { label: 'Publishing Events from Aggregate Roots', slug: 'repositories/core-domain-events' }, { label: 'Null Handling of Repository Methods', slug: 'repositories/null-handling' }, - { label: 'CDI Integration', slug: 'redis/redis-repositories/cdi-integration' }, + { label: 'CDI Integration', slug: 'valkey/valkey-repositories/cdi-integration' }, { label: 'Repository query keywords', slug: 'repositories/query-keywords-reference' }, { label: 'Repository query return types', slug: 'repositories/query-return-types-reference' }, ] diff --git a/docs/src/content/docs/appendix.md b/docs/src/content/docs/appendix.md index c233c9401..1b3911268 100644 --- a/docs/src/content/docs/appendix.md +++ b/docs/src/content/docs/appendix.md @@ -5,11 +5,11 @@ description: Additional reference information ## Schema -[Spring Data Redis Schema (redis-namespace)](https://www.springframework.org/schema/redis/spring-redis-1.0.xsd) +[Spring Data Valkey Schema (valkey-namespace)](https://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd) ## Supported Commands -*Table 1. Redis commands supported by `RedisTemplate`* +*Table 1. Valkey commands supported by `ValkeyTemplate`* | Command | Template Support | |---------|------------------| diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md index ec4ffd6ec..f3ad460aa 100644 --- a/docs/src/content/docs/commons/migration.md +++ b/docs/src/content/docs/commons/migration.md @@ -11,16 +11,16 @@ This section contains details about migration steps, deprecations, and removals. | Type | Replacement | |------|-------------| -| o.s.d.redis.Version | o.s.d.util.Version | -| o.s.d.redis.VersionParser | - | -| o.s.d.redis.connection.RedisZSetCommands.Aggregate | o.s.d.redis.connection.zset.Aggregate | -| o.s.d.redis.connection.RedisZSetCommands.Tuple | o.s.d.redis.connection.zset.Tuple | -| o.s.d.redis.connection.RedisZSetCommands.Weights | o.s.d.redis.connection.zset.Weights | -| o.s.d.redis.connection.RedisZSetCommands.Range | o.s.d.domain.Range | -| o.s.d.redis.connection.RedisZSetCommands.Limit | o.s.d.redis.connection.Limit.java | -| o.s.d.redis.connection.jedis.JedisUtils | - | -| o.s.d.redis.connection.jedis.JedisVersionUtil | - | -| o.s.d.redis.core.convert.CustomConversions | o.s.d.convert.CustomConversions | +| o.s.d.valkey.Version | o.s.d.util.Version | +| o.s.d.valkey.VersionParser | - | +| o.s.d.valkey.connection.ValkeyZSetCommands.Aggregate | o.s.d.valkey.connection.zset.Aggregate | +| o.s.d.valkey.connection.ValkeyZSetCommands.Tuple | o.s.d.valkey.connection.zset.Tuple | +| o.s.d.valkey.connection.ValkeyZSetCommands.Weights | o.s.d.valkey.connection.zset.Weights | +| o.s.d.valkey.connection.ValkeyZSetCommands.Range | o.s.d.domain.Range | +| o.s.d.valkey.connection.ValkeyZSetCommands.Limit | o.s.d.valkey.connection.Limit.java | +| o.s.d.valkey.connection.jedis.JedisUtils | - | +| o.s.d.valkey.connection.jedis.JedisVersionUtil | - | +| o.s.d.valkey.core.convert.CustomConversions | o.s.d.convert.CustomConversions | ### Changed Methods and Types @@ -28,69 +28,69 @@ This section contains details about migration steps, deprecations, and removals. | Type | Method | Replacement | |------|--------|-------------| -| o.s.d.redis.core.Cursor | open | - | -| o.s.d.redis.core.RedisTemplate | execute | doWithKeys | -| o.s.d.redis.stream.StreamMessageListenerContainer | isAutoAck | isAutoAcknowledge | -| o.s.d.redis.stream.StreamMessageListenerContainer | autoAck | autoAcknowledge | +| o.s.d.valkey.core.Cursor | open | - | +| o.s.d.valkey.core.ValkeyTemplate | execute | doWithKeys | +| o.s.d.valkey.stream.StreamMessageListenerContainer | isAutoAck | isAutoAcknowledge | +| o.s.d.valkey.stream.StreamMessageListenerContainer | autoAck | autoAcknowledge | -*Table 2. Redis Connection* +*Table 2. Valkey Connection* | Type | Method | Replacement | |------|--------|-------------| -| o.s.d.redis.connection.ClusterCommandExecutionFailureException | getCauses | getSuppressed | -| o.s.d.redis.connection.RedisConnection | bgWriteAof | bgReWriteAof | -| o.s.d.redis.connection.RedisConnection | slaveOf | replicaOf | -| o.s.d.redis.connection.RedisConnection | slaveOfNoOne | replicaOfNoOne | -| o.s.d.redis.connection.ReactiveClusterCommands | clusterGetSlaves | clusterGetReplicas | -| o.s.d.redis.connection.ReactiveClusterCommands | clusterGetMasterSlaveMap | clusterGetMasterReplicaMap | -| o.s.d.redis.connection.ReactiveKeyCommands | getNewName | getNewKey | -| o.s.d.redis.connection.RedisClusterNode.Flag | SLAVE | REPLICA | -| o.s.d.redis.connection.RedisClusterNode.Builder | slaveOf | replicaOf | -| o.s.d.redis.connection.RedisNode | isSlave | isReplica | -| o.s.d.redis.connection.RedisSentinelCommands | slaves | replicas | -| o.s.d.redis.connection.RedisServer | getNumberSlaves | getNumberReplicas | -| o.s.d.redis.connection.RedisServerCommands | slaveOf | replicaOf | -| o.s.d.redis.core.ClusterOperations | getSlaves | getReplicas | -| o.s.d.redis.core.RedisOperations | slaveOf | replicaOf | - -*Table 3. Redis Operations* +| o.s.d.valkey.connection.ClusterCommandExecutionFailureException | getCauses | getSuppressed | +| o.s.d.valkey.connection.ValkeyConnection | bgWriteAof | bgReWriteAof | +| o.s.d.valkey.connection.ValkeyConnection | slaveOf | replicaOf | +| o.s.d.valkey.connection.ValkeyConnection | slaveOfNoOne | replicaOfNoOne | +| o.s.d.valkey.connection.ReactiveClusterCommands | clusterGetSlaves | clusterGetReplicas | +| o.s.d.valkey.connection.ReactiveClusterCommands | clusterGetMasterSlaveMap | clusterGetMasterReplicaMap | +| o.s.d.valkey.connection.ReactiveKeyCommands | getNewName | getNewKey | +| o.s.d.valkey.connection.ValkeyClusterNode.Flag | SLAVE | REPLICA | +| o.s.d.valkey.connection.ValkeyClusterNode.Builder | slaveOf | replicaOf | +| o.s.d.valkey.connection.ValkeyNode | isSlave | isReplica | +| o.s.d.valkey.connection.ValkeySentinelCommands | slaves | replicas | +| o.s.d.valkey.connection.ValkeyServer | getNumberSlaves | getNumberReplicas | +| o.s.d.valkey.connection.ValkeyServerCommands | slaveOf | replicaOf | +| o.s.d.valkey.core.ClusterOperations | getSlaves | getReplicas | +| o.s.d.valkey.core.ValkeyOperations | slaveOf | replicaOf | + +*Table 3. Valkey Operations* | Type | Method | Replacement | |------|--------|-------------| -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoAdd | add | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoDist | distance | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoHash | hash | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoPos | position | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRadius | radius | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRadiusByMember | radius | -| o.s.d.redis.core.GeoOperations & BoundGeoOperations | geoRemove | remove | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoAdd | add | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoDist | distance | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoHash | hash | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoPos | position | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRadius | radius | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRadiusByMember | radius | +| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRemove | remove | -*Table 4. Redis Cache* +*Table 4. Valkey Cache* | Type | Method | Replacement | |------|--------|-------------| -| o.s.d.redis.cache.RedisCacheConfiguration | prefixKeysWith | prefixCacheNameWith | -| o.s.d.redis.cache.RedisCacheConfiguration | getKeyPrefix | getKeyPrefixFor | +| o.s.d.valkey.cache.ValkeyCacheConfiguration | prefixKeysWith | prefixCacheNameWith | +| o.s.d.valkey.cache.ValkeyCacheConfiguration | getKeyPrefix | getKeyPrefixFor | ### Jedis -Please read the Jedis [upgrading guide](https://github.com/redis/jedis/blob/v4.0.0/docs/3to4.md) which covers important driver changes. +Please read the Jedis [upgrading guide](https://github.com/valkey/jedis/blob/v4.0.0/docs/3to4.md) which covers important driver changes. -*Table 5. Jedis Redis Connection* +*Table 5. Jedis Valkey Connection* | Type | Method | Replacement | |------|--------|-------------| -| o.s.d.redis.connection.jedis.JedisConnectionFactory | getShardInfo | _can be obtained via JedisClientConfiguration_ | -| o.s.d.redis.connection.jedis.JedisConnectionFactory | setShardInfo | _can be set via JedisClientConfiguration_ | -| o.s.d.redis.connection.jedis.JedisConnectionFactory | createCluster | _now requires a `Connection` instead of `Jedis` instance_ | -| o.s.d.redis.connection.jedis.JedisConverters | | has package visibility now | -| o.s.d.redis.connection.jedis.JedisConverters | tuplesToTuples | - | -| o.s.d.redis.connection.jedis.JedisConverters | stringListToByteList | - | -| o.s.d.redis.connection.jedis.JedisConverters | stringSetToByteSet | - | -| o.s.d.redis.connection.jedis.JedisConverters | stringMapToByteMap | - | -| o.s.d.redis.connection.jedis.JedisConverters | tupleSetToTupleSet | - | -| o.s.d.redis.connection.jedis.JedisConverters | toTupleSet | - | -| o.s.d.redis.connection.jedis.JedisConverters | toDataAccessException | o.s.d.redis.connection.jedis.JedisExceptionConverter#convert | +| o.s.d.valkey.connection.jedis.JedisConnectionFactory | getShardInfo | _can be obtained via JedisClientConfiguration_ | +| o.s.d.valkey.connection.jedis.JedisConnectionFactory | setShardInfo | _can be set via JedisClientConfiguration_ | +| o.s.d.valkey.connection.jedis.JedisConnectionFactory | createCluster | _now requires a `Connection` instead of `Jedis` instance_ | +| o.s.d.valkey.connection.jedis.JedisConverters | | has package visibility now | +| o.s.d.valkey.connection.jedis.JedisConverters | tuplesToTuples | - | +| o.s.d.valkey.connection.jedis.JedisConverters | stringListToByteList | - | +| o.s.d.valkey.connection.jedis.JedisConverters | stringSetToByteSet | - | +| o.s.d.valkey.connection.jedis.JedisConverters | stringMapToByteMap | - | +| o.s.d.valkey.connection.jedis.JedisConverters | tupleSetToTupleSet | - | +| o.s.d.valkey.connection.jedis.JedisConverters | toTupleSet | - | +| o.s.d.valkey.connection.jedis.JedisConverters | toDataAccessException | o.s.d.valkey.connection.jedis.JedisExceptionConverter#convert | #### Transactions / Pipelining diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index c4f1f655a..00ee282f4 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -1,9 +1,9 @@ --- -title: Spring Data Redis -description: Spring Data Redis provides Redis connectivity and repository support for the Redis database. It eases development of applications with a consistent programming model that need to access Redis data sources. +title: Spring Data Valkey +description: Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. It eases development of applications with a consistent programming model that need to access Valkey data sources. template: splash hero: - tagline: Redis connectivity and repository support for Spring applications + tagline: Valkey connectivity and repository support for Spring applications actions: - text: Overview link: /overview/ @@ -19,13 +19,13 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; ## Quick Navigation - - Redis connectivity, templates, and operations + + Valkey connectivity, templates, and operations - [Learn more →](/redis/) + [Learn more →](/valkey/) - Repository abstraction for Redis data access + Repository abstraction for Valkey data access [Learn more →](/repositories/) @@ -35,7 +35,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; [Learn more →](/observability/) - Upgrade from Spring Data Redis + Upgrade from Spring Data Valkey [Learn more →](/commons/migration/) diff --git a/docs/src/content/docs/observability.md b/docs/src/content/docs/observability.md index b847ee462..682ba0147 100644 --- a/docs/src/content/docs/observability.md +++ b/docs/src/content/docs/observability.md @@ -4,8 +4,8 @@ description: Observability Integration for monitoring and metrics --- Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. -Spring Data Redis ships with a Micrometer integration through the Lettuce driver to collect observations during Redis interaction. -Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Redis command. +Spring Data Valkey ships with a Micrometer integration through the Lettuce driver to collect observations during Valkey interaction. +Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Valkey command. To enable the integration, apply the following configuration to `LettuceClientConfiguration`: @@ -17,7 +17,7 @@ class ObservabilityConfiguration { public ClientResources clientResources(ObservationRegistry observationRegistry) { return ClientResources.builder() - .tracing(new MicrometerTracingAdapter(observationRegistry, "my-redis-cache")) + .tracing(new MicrometerTracingAdapter(observationRegistry, "my-valkey-cache")) .build(); } @@ -26,34 +26,34 @@ class ObservabilityConfiguration { LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() .clientResources(clientResources).build(); - RedisConfiguration redisConfiguration = …; - return new LettuceConnectionFactory(redisConfiguration, clientConfig); + ValkeyConfiguration valkeyConfiguration = …; + return new LettuceConnectionFactory(valkeyConfiguration, clientConfig); } } ``` -See also [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#redis) for further reference. +See also [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/#valkey) for further reference. ## Observability - Metrics Below you can find a list of all metrics declared by this project. -### Redis Command Observation +### Valkey Command Observation -> Timer created around a Redis command execution. +> Timer created around a Valkey command execution. -**Metric name** `spring.data.redis`. **Type** `timer` and **base unit** `seconds`. +**Metric name** `spring.data.valkey`. **Type** `timer` and **base unit** `seconds`. -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. +Fully qualified name of the enclosing class `io.valkey.springframework.data.connection.lettuce.observability.ValkeyObservation`. *Table 1. Low cardinality Keys* | Name | Description | |------|-------------| -| `db.operation` | Redis command value. | -| `db.redis.database_index` | Redis database index. | +| `db.operation` | Valkey command value. | +| `db.valkey.database_index` | Valkey database index. | | `db.system` | Database system. | -| `db.user` | Redis user. | +| `db.user` | Valkey user. | | `net.peer.name` | Name of the database host. | | `net.peer.port` | Logical remote port number. | | `net.sock.peer.addr` | Mongo peer address. | @@ -64,33 +64,33 @@ Fully qualified name of the enclosing class `org.springframework.data.redis.conn | Name | Description | |------|-------------| -| `db.statement` | Redis statement. | -| `spring.data.redis.command.error` | Redis error response. | +| `db.statement` | Valkey statement. | +| `spring.data.valkey.command.error` | Valkey error response. | ## Observability - Spans Below you can find a list of all spans declared by this project. -### Redis Command Observation Span +### Valkey Command Observation Span -> Timer created around a Redis command execution. +> Timer created around a Valkey command execution. -**Span name** `spring.data.redis`. +**Span name** `spring.data.valkey`. -Fully qualified name of the enclosing class `org.springframework.data.redis.connection.lettuce.observability.RedisObservation`. +Fully qualified name of the enclosing class `io.valkey.springframework.data.connection.lettuce.observability.ValkeyObservation`. *Table 3. Tag Keys* | Name | Description | |------|-------------| -| `db.operation` | Redis command value. | -| `db.redis.database_index` | Redis database index. | -| `db.statement` | Redis statement. | +| `db.operation` | Valkey command value. | +| `db.valkey.database_index` | Valkey database index. | +| `db.statement` | Valkey statement. | | `db.system` | Database system. | -| `db.user` | Redis user. | +| `db.user` | Valkey user. | | `net.peer.name` | Name of the database host. | | `net.peer.port` | Logical remote port number. | | `net.sock.peer.addr` | Mongo peer address. | | `net.sock.peer.port` | Mongo peer port. | | `net.transport` | Network transport. | -| `spring.data.redis.command.error` | Redis error response. | +| `spring.data.valkey.command.error` | Valkey error response. | diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index a1a0e2c15..72a174c30 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -1,15 +1,15 @@ --- -title: Spring Data Redis -description: Spring Data Redis provides Redis connectivity and repository support for the Redis database. +title: Spring Data Valkey +description: Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. --- -_Spring Data Redis provides Redis connectivity and repository support for the Redis database. It eases development of applications with a consistent programming model that need to access Redis data sources._ +_Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. It eases development of applications with a consistent programming model that need to access Valkey data sources._ | Section | Description | |---------|-------------| -| [Redis](../redis) | Redis support and connectivity | -| [Redis Repositories](../repositories) | Redis Repositories | +| [Valkey](../valkey) | Valkey support and connectivity | +| [Valkey Repositories](../repositories) | Valkey Repositories | | [Observability](../observability) | Observability Integration | | [Wiki](https://github.com/spring-projects/spring-data-commons/wiki) | What's New, Upgrade Notes, Supported Versions, additional cross-version information. | diff --git a/docs/src/content/docs/preface.md b/docs/src/content/docs/preface.md index cf697549c..ebf2d646c 100644 --- a/docs/src/content/docs/preface.md +++ b/docs/src/content/docs/preface.md @@ -1,13 +1,13 @@ --- title: Preface -description: Introduction to Spring Data Redis +description: Introduction to Spring Data Valkey --- -The Spring Data Redis project applies core Spring concepts to the development of solutions by using a key-value style data store. +The Spring Data Valkey project applies core Spring concepts to the development of solutions by using a key-value style data store. We provide a "template" as a high-level abstraction for sending and receiving messages. You may notice similarities to the JDBC support in the Spring Framework. -This section provides an easy-to-follow guide for getting started with the Spring Data Redis module. +This section provides an easy-to-follow guide for getting started with the Spring Data Valkey module. ## Learning Spring @@ -22,15 +22,15 @@ Spring Data uses Spring framework's [core](https://docs.spring.io/spring-framewo While you need not know the Spring APIs, understanding the concepts behind them is important. At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. -The core functionality of the Redis support can be used directly, with no need to invoke the IoC services of the Spring Container. +The core functionality of the Valkey support can be used directly, with no need to invoke the IoC services of the Spring Container. This is much like `JdbcTemplate`, which can be used "standalone" without any other services of the Spring container. -To leverage all the features of Spring Data Redis, such as the repository support, you need to configure some parts of the library to use Spring. +To leverage all the features of Spring Data Valkey, such as the repository support, you need to configure some parts of the library to use Spring. To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. There are a lot of articles, blog entries, and books on the subject. See the Spring framework [home page](https://spring.io/projects/spring-framework/) for more information. -In general, this should be the starting point for developers wanting to try Spring Data Redis. +In general, this should be the starting point for developers wanting to try Spring Data Valkey. ## Learning NoSQL and Key Value Stores @@ -41,19 +41,19 @@ It usually does not take more then five to ten minutes to go through them and, i ### Trying out the Samples -One can find various samples for key-value stores in the dedicated Spring Data example repo, at [https://github.com/spring-projects/spring-data-examples/tree/main/redis](https://github.com/spring-projects/spring-data-examples/tree/main/redis). +One can find various samples for key-value stores in the dedicated Spring Data example repo, at [https://github.com/spring-projects/spring-data-examples/tree/main/valkey](https://github.com/spring-projects/spring-data-examples/tree/main/valkey). ## Requirements -Spring Data Redis binaries require JDK level 17 and above and [Spring Framework](https://spring.io/projects/spring-framework/) 6.0 and above. +Spring Data Valkey binaries require JDK level 17 and above and [Spring Framework](https://spring.io/projects/spring-framework/) 6.0 and above. -In terms of key-value stores, [Redis](https://redis.io) 2.6.x or higher is required. -Spring Data Redis is currently tested against the latest 6.0 release. +In terms of key-value stores, [Valkey](https://valkey.io) 2.6.x or higher is required. +Spring Data Valkey is currently tested against the latest 6.0 release. ## Additional Help Resources Learning a new framework is not always straightforward. -In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data Redis module. +In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data Valkey module. However, if you encounter issues or you need advice, feel free to use one of the following links: ### Community Forum @@ -70,9 +70,9 @@ Professional, from-the-source support, with guaranteed response time, is availab For information on the Spring Data source code repository, nightly builds, and snapshot artifacts, see the Spring Data home [page](https://spring.io/projects/spring-data/). You can help make Spring Data best serve the needs of the Spring community by interacting with developers on Stack Overflow at either -[spring-data](https://stackoverflow.com/questions/tagged/spring-data) or [spring-data-redis](https://stackoverflow.com/questions/tagged/spring-data-redis). +[spring-data](https://stackoverflow.com/questions/tagged/spring-data) or [spring-data-valkey](https://stackoverflow.com/questions/tagged/spring-data-valkey). -If you encounter a bug or want to suggest an improvement (including to this documentation), please create a ticket on [Github](https://github.com/spring-projects/spring-data-redis/issues/new). +If you encounter a bug or want to suggest an improvement (including to this documentation), please create a ticket on [Github](https://github.com/valkey-io/spring-data-valkey/issues/new). To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community [Portal](https://spring.io/). diff --git a/docs/src/content/docs/redis.md b/docs/src/content/docs/redis.md deleted file mode 100644 index 3cbbfec99..000000000 --- a/docs/src/content/docs/redis.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Redis Overview -description: Spring Data Redis support and connectivity ---- - -One of the key-value stores supported by Spring Data is [Redis](https://redis.io). -To quote the Redis project home page: - -> Redis is an advanced key-value store. -> It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. -> All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. -> Redis supports different kind of sorting abilities. - -Spring Data Redis provides easy configuration and access to Redis from Spring applications. -It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns. - -Spring Data support for Redis contains a wide range of features: - -* [`RedisTemplate` and `ReactiveRedisTemplate` helper class](/redis/template) that increases productivity when performing common Redis operations. -Includes integrated serialization between objects and values. -* Exception translation into Spring's portable Data Access Exception hierarchy. -* Automatic implementation of [Repository interfaces](/repositories), including support for custom query methods. -* Feature-rich [Object Mapping](/redis/redis-repositories/mapping) integrated with Spring's Conversion Service. -* Annotation-based mapping metadata that is extensible to support other metadata formats. -* [Transactions](/redis/transactions) and [Pipelining](/redis/pipelining). -* [Redis Cache](/redis/redis-cache) integration through Spring's Cache abstraction. -* [Redis Pub/Sub Messaging](/redis/pubsub) and [Redis Stream](/redis/redis-streams) Listeners. -* [Redis Collection Implementations](/redis/support-classes) for Java such as `RedisList` or `RedisSet`. - -## Why Spring Data Redis? - -The Spring Framework is the leading full-stack Java/JEE application framework. -It provides a lightweight container and a non-invasive programming model enabled by the use of dependency injection, AOP, and portable service abstractions. - -[NoSQL](https://en.wikipedia.org/wiki/NoSQL) storage systems provide an alternative to classical RDBMS for horizontal scalability and speed. -In terms of implementation, key-value stores represent one of the largest (and oldest) members in the NoSQL space. - -The Spring Data Redis (SDR) framework makes it easy to write Spring applications that use the Redis key-value store by eliminating the redundant tasks and boilerplate code required for interacting with the store through Spring's excellent infrastructure support. - -## Redis Support High-level View - -The Redis support provides several components.For most tasks, the high-level abstractions and support services are the best choice.Note that, at any point, you can move between layers.For example, you can get a low-level connection (or even the native library) to communicate directly with Redis. diff --git a/docs/src/content/docs/redis/connection-modes.md b/docs/src/content/docs/redis/connection-modes.md deleted file mode 100644 index 41a29bb90..000000000 --- a/docs/src/content/docs/redis/connection-modes.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: Connection Modes -description: Connection Modes documentation ---- - -Redis can be operated in various setups. -Each mode of operation requires specific configuration that is explained in the following sections. - -## Redis Standalone - -The easiest way to get started is by using Redis Standalone with a single Redis server, - -Configure `org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory` or `org.springframework.data.redis.connection.jedis.JedisConnectionFactory`, as shown in the following example: - -```java -@Configuration -class RedisStandaloneConfiguration { - - /** - * Lettuce - */ - @Bean - public RedisConnectionFactory lettuceConnectionFactory() { - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); - } - - /** - * Jedis - */ - @Bean - public RedisConnectionFactory jedisConnectionFactory() { - return new JedisConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); - } -} -``` - -## Write to Master, Read from Replica - -The Redis Master/Replica setup -- without automatic failover (for automatic failover see: [Sentinel](/redis/connection-modes#redis-sentinel)) -- not only allows data to be safely stored at more nodes. -It also allows, by using [Lettuce](/redis/drivers#configuring-the-lettuce-connector), reading data from replicas while pushing writes to the master. -You can set the read/write strategy to be used by using `LettuceClientConfiguration`, as shown in the following example: - -```java -@Configuration -class WriteToMasterReadFromReplicaConfiguration { - - @Bean - public LettuceConnectionFactory redisConnectionFactory() { - - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - .readFrom(REPLICA_PREFERRED) - .build(); - - RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("server", 6379); - - return new LettuceConnectionFactory(serverConfig, clientConfig); - } -} -``` - -:::tip -For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration` instead of `org.springframework.data.redis.connection.RedisStandaloneConfiguration`. Please note that `RedisStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. -::: - -## Redis Sentinel - -For dealing with high-availability Redis, Spring Data Redis has support for [Redis Sentinel](https://redis.io/topics/sentinel), using `org.springframework.data.redis.connection.RedisSentinelConfiguration`, as shown in the following example: - -```java -/** - * Lettuce - */ -@Bean -public RedisConnectionFactory lettuceConnectionFactory() { - RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() - .master("mymaster") - .sentinel("127.0.0.1", 26379) - .sentinel("127.0.0.1", 26380); - return new LettuceConnectionFactory(sentinelConfig); -} - -/** - * Jedis - */ -@Bean -public RedisConnectionFactory jedisConnectionFactory() { - RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() - .master("mymaster") - .sentinel("127.0.0.1", 26379) - .sentinel("127.0.0.1", 26380); - return new JedisConnectionFactory(sentinelConfig); -} -``` - -:::tip -`RedisSentinelConfiguration` can also be defined through `RedisSentinelConfiguration.of(PropertySource)`, which lets you pick up the following properties: - -*Configuration Properties* - -* `spring.redis.sentinel.master`: name of the master node. -* `spring.redis.sentinel.nodes`: Comma delimited list of host:port pairs. -* `spring.redis.sentinel.username`: The username to apply when authenticating with Redis Sentinel (requires Redis 6) -* `spring.redis.sentinel.password`: The password to apply when authenticating with Redis Sentinel -* `spring.redis.sentinel.dataNode.username`: The username to apply when authenticating with Redis Data Node -* `spring.redis.sentinel.dataNode.password`: The password to apply when authenticating with Redis Data Node -* `spring.redis.sentinel.dataNode.database`: The database index to apply when authenticating with Redis Data Node -::: - -Sometimes, direct interaction with one of the Sentinels is required. Using `RedisConnectionFactory.getSentinelConnection()` or `RedisConnection.getSentinelCommands()` gives you access to the first active Sentinel configured. - -## Redis Cluster - -[Cluster support](/redis/cluster) is based on the same building blocks as non-clustered communication. `org.springframework.data.redis.connection.RedisClusterConnection`, an extension to `RedisConnection`, handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. -`RedisClusterConnection` instances are created with the `RedisConnectionFactory`, which has to be set up with the associated `org.springframework.data.redis.connection.RedisClusterConfiguration`, as shown in the following example: - -*Example 1. Sample RedisConnectionFactory Configuration for Redis Cluster* - -```java -@Component -@ConfigurationProperties(prefix = "spring.redis.cluster") -public class ClusterConfigurationProperties { - - /* - * spring.redis.cluster.nodes[0] = 127.0.0.1:7379 - * spring.redis.cluster.nodes[1] = 127.0.0.1:7380 - * ... - */ - List nodes; - - /** - * Get initial collection of known cluster nodes in format {@code host:port}. - * - * @return - */ - public List getNodes() { - return nodes; - } - - public void setNodes(List nodes) { - this.nodes = nodes; - } -} - -@Configuration -public class AppConfig { - - /** - * Type safe representation of application.properties - */ - @Autowired ClusterConfigurationProperties clusterProperties; - - public @Bean RedisConnectionFactory connectionFactory() { - - return new LettuceConnectionFactory( - new RedisClusterConfiguration(clusterProperties.getNodes())); - } -} -``` - -:::tip -`RedisClusterConfiguration` can also be defined through `RedisClusterConfiguration.of(PropertySource)`, which lets you pick up the following properties: - -*Configuration Properties* - -* `spring.redis.cluster.nodes`: Comma-delimited list of host:port pairs. -* `spring.redis.cluster.max-redirects`: Number of allowed cluster redirections. -::: - -:::note -The initial configuration points driver libraries to an initial set of cluster nodes. Changes resulting from live cluster reconfiguration are kept only in the native driver and are not written back to the configuration. -::: diff --git a/docs/src/content/docs/redis/getting-started.mdx b/docs/src/content/docs/redis/getting-started.mdx deleted file mode 100644 index a4d2aefbd..000000000 --- a/docs/src/content/docs/redis/getting-started.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Getting Started -description: Getting Started documentation ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-redis) or create a Spring project in [Spring Tools](https://spring.io/tools). -## Examples Repository - -The GitHub [spring-data-examples repository](https://github.com/spring-projects/spring-data-examples) hosts several examples that you can download and play around with to get a feel for how the library works. -## Hello World - -First, you need to set up a running Redis server. -Spring Data Redis requires Redis 2.6 or above and Spring Data Redis integrates with [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/redis/jedis), two popular open-source Java libraries for Redis. - -Now you can create a simple Java application that stores and reads a value to and from Redis. - -Create the main application to run, as the following example shows: - - - - -```java -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; - -public class RedisApplication { - - private static final Log LOG = LogFactory.getLog(RedisApplication.class); - - public static void main(String[] args) { - - RedisConnectionFactory factory = new LettuceConnectionFactory(); - - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(factory); - template.afterPropertiesSet(); - - template.opsForValue().set("foo", "bar"); - - LOG.info("Value at foo:" + template.opsForValue().get("foo")); - - template.getConnectionFactory().getConnection().close(); - } -} -``` - - - - -```java -import reactor.core.publisher.Mono; - -import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.ReactiveRedisTemplate; -import org.springframework.data.redis.serialization.RedisSerializationContext; - -public class ReactiveRedisApplication { - - public static void main(String[] args) { - - ReactiveRedisConnectionFactory factory = new LettuceConnectionFactory(); - - ReactiveRedisTemplate template = new ReactiveRedisTemplate<>(factory, RedisSerializationContext.string()); - - template.opsForValue().set("foo", "bar") - .then(template.opsForValue().get("foo")) - .doOnNext(System.out::println) - .then(Mono.fromRunnable(() -> factory.getReactiveConnection().close())) - .subscribe(); - } -} -``` - - - -Even in this simple example, there are a few notable things to point out: - -* You can create an instance of `org.springframework.data.redis.core.RedisTemplate` (or `org.springframework.data.redis.core.ReactiveRedisTemplate`for reactive usage) with a `org.springframework.data.redis.connection.RedisConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. -* There's no single way to use Redis as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/redis/pipelining.md b/docs/src/content/docs/redis/pipelining.md deleted file mode 100644 index 938b3887e..000000000 --- a/docs/src/content/docs/redis/pipelining.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Pipelining -description: Pipelining documentation ---- - -Redis provides support for [pipelining](https://redis.io/topics/pipelining), which involves sending multiple commands to the server without waiting for the replies and then reading the replies in a single step. Pipelining can improve performance when you need to send several commands in a row, such as adding many elements to the same List. - -Spring Data Redis provides several `RedisTemplate` methods for running commands in a pipeline. If you do not care about the results of the pipelined operations, you can use the standard `execute` method, passing `true` for the `pipeline` argument. The `executePipelined` methods run the provided `RedisCallback` or `SessionCallback` in a pipeline and return the results, as shown in the following example: - -```java -//pop a specified number of items from a queue -List results = stringRedisTemplate.executePipelined( - new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - StringRedisConnection stringRedisConn = (StringRedisConnection)connection; - for(int i=0; i< batchSize; i++) { - stringRedisConn.rPop("myqueue"); - } - return null; - } -}); -``` - -The preceding example runs a bulk right pop of items from a queue in a pipeline. -The `results` `List` contains all the popped items. `RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results before returning, so the returned items in the preceding example are Strings. -There are additional `executePipelined` methods that let you pass a custom serializer for pipelined results. - -Note that the value returned from the `RedisCallback` is required to be `null`, as this value is discarded in favor of returning the results of the pipelined commands. - -:::tip -The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. -::: - -```java -LettuceConnectionFactory factory = // ... -factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3)); // (1) -``` -```text -1. Buffer locally and flush after every 3rd command. -``` - -:::note -Pipelining is limited to Redis Standalone. -::: - -Redis Cluster is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. -Same-slot keys are fully supported. diff --git a/docs/src/content/docs/redis/redis-cache.md b/docs/src/content/docs/redis/redis-cache.md deleted file mode 100644 index 1e9aea5e3..000000000 --- a/docs/src/content/docs/redis/redis-cache.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: Redis Cache -description: Redis Cache documentation ---- - -Spring Data Redis provides an implementation of Spring Framework's [Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration.html#cache) in the `org.springframework.data.redis.cache` package. -To use Redis as a backing implementation, add `org.springframework.data.redis.cache.RedisCacheManager` to your configuration, as follows: - -```java -@Bean -public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { - return RedisCacheManager.create(connectionFactory); -} -``` - -`RedisCacheManager` behavior can be configured with `org.springframework.data.redis.cache.RedisCacheManager$RedisCacheManagerBuilder`, letting you set the default `org.springframework.data.redis.cache.RedisCacheManager`, transaction behavior, and predefined caches. - -```java -RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - .transactionAware() - .withInitialCacheConfigurations(Collections.singletonMap("predefined", - RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues())) - .build(); -``` - -As shown in the preceding example, `RedisCacheManager` allows custom configuration on a per-cache basis. - -The behavior of `org.springframework.data.redis.cache.RedisCache` created by `org.springframework.data.redis.cache.RedisCacheManager` is defined with `RedisCacheConfiguration`. -The configuration lets you set key expiration times, prefixes, and `RedisSerializer` implementations for converting to and from the binary storage format, as shown in the following example: - -```java -RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofSeconds(1)) - .disableCachingNullValues(); -``` - -`org.springframework.data.redis.cache.RedisCacheManager` defaults to a lock-free `org.springframework.data.redis.cache.RedisCacheWriter` for reading and writing binary values. -Lock-free caching improves throughput. -The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Redis. -The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. - -Locking applies on the *cache level*, not per *cache entry*. - -It is possible to opt in to the locking behavior as follows: - -```java -RedisCacheManager cacheManager = RedisCacheManager - .builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory)) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - ... -``` - -By default, any `key` for a cache entry gets prefixed with the actual cache name followed by two colons. -This behavior can be changed to a static as well as a computed prefix. - -The following example shows how to set a static prefix: - -```java -// static key prefix -RedisCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)"); -``` - -The following example shows how to set a computed prefix: - -```java -// computed key prefix -RedisCacheConfiguration.defaultCacheConfig() - .computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName); -``` - -The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `KEYS` can cause performance issues with large keyspaces. -Therefore, the default `RedisCacheWriter` can be created with a `BatchStrategy` to switch to a `SCAN`-based batch strategy. -The `SCAN` strategy requires a batch size to avoid excessive Redis command round trips: - -```java -RedisCacheManager cacheManager = RedisCacheManager - .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) - .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()) - ... -``` - -:::note -The `KEYS` batch strategy is fully supported using any driver and Redis operation mode (Standalone, Clustered). -::: - -`SCAN` is fully supported when using the Lettuce driver. -Jedis supports `SCAN` only in non-clustered modes. - -The following table lists the default settings for `RedisCacheManager`: - -*Table 1. `RedisCacheManager` defaults* - -| Setting | Value | -|---------|-------| -| Cache Writer | Non-locking, `KEYS` batch strategy | -| Cache Configuration | `RedisCacheConfiguration#defaultConfiguration` | -| Initial Caches | None | -| Transaction Aware | No | - -The following table lists the default settings for `RedisCacheConfiguration`: - -*Table 2. RedisCacheConfiguration defaults* - -| Setting | Value | -|---------|-------| -| Key Expiration | None | -| Cache `null` | Yes | -| Prefix Keys | Yes | -| Default Prefix | The actual cache name | -| Key Serializer | `StringRedisSerializer` | -| Value Serializer | `JdkSerializationRedisSerializer` | -| Conversion Service | `DefaultFormattingConversionService` with default cache key converters | - -:::note -By default `RedisCache`, statistics are disabled. -::: - -Use `RedisCacheManagerBuilder.enableStatistics()` to collect local _hits_ and _misses_ through `RedisCache#getStatistics()`, returning a snapshot of the collected data. - -## Redis Cache Expiration - -The implementation of time-to-idle (TTI) as well as time-to-live (TTL) varies in definition and behavior even across different data stores. - -In general: - -* _time-to-live_ (TTL) _expiration_ - TTL is only set and reset by a create or update data access operation. -As long as the entry is written before the TTL expiration timeout, including on creation, an entry's timeout will reset to the configured duration of the TTL expiration timeout. -For example, if the TTL expiration timeout is set to 5 minutes, then the timeout will be set to 5 minutes on entry creation and reset to 5 minutes anytime the entry is updated thereafter and before the 5-minute interval expires. -If no update occurs within 5 minutes, even if the entry was read several times, or even just read once during the 5-minute interval, the entry will still expire. -The entry must be written to prevent the entry from expiring when declaring a TTL expiration policy. - -* _time-to-idle_ (TTI) _expiration_ - TTI is reset anytime the entry is also read as well as for entry updates, and is effectively and extension to the TTL expiration policy. - -:::note -Some data stores expire an entry when TTL is configured no matter what type of data access operation occurs on the entry (reads, writes, or otherwise). -After the set, configured TTL expiration timeout, the entry is evicted from the data store regardless. -Eviction actions (for example: destroy, invalidate, overflow-to-disk (for persistent stores), etc.) are data store specific. -::: - -### Time-To-Live (TTL) Expiration - -Spring Data Redis's `Cache` implementation supports _time-to-live_ (TTL) expiration on cache entries. -Users can either configure the TTL expiration timeout with a fixed `Duration` or a dynamically computed `Duration` per cache entry by supplying an implementation of the new `RedisCacheWriter.TtlFunction` interface. - -:::tip -The `RedisCacheWriter.TtlFunction` interface was introduced in Spring Data Redis `3.2.0`. -::: - -If all cache entries should expire after a set duration of time, then simply configure a TTL expiration timeout with a fixed `Duration`, as follows: - -```java -RedisCacheConfiguration fiveMinuteTtlExpirationDefaults = - RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)); -``` - -However, if the TTL expiration timeout should vary by cache entry, then you must provide a custom implementation of the `RedisCacheWriter.TtlFunction` interface: - -```java -enum MyCustomTtlFunction implements TtlFunction { - - INSTANCE; - - @Override - public Duration getTimeToLive(Object key, @Nullable Object value) { - // compute a TTL expiration timeout (Duration) based on the cache entry key and/or value - } -} -``` - -:::note -Under-the-hood, a fixed `Duration` TTL expiration is wrapped in a `TtlFunction` implementation returning the provided `Duration`. -::: - -Then, you can either configure the fixed `Duration` or the dynamic, per-cache entry `Duration` TTL expiration on a global basis using: - -*Global fixed Duration TTL expiration timeout* - -```java -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(fiveMinuteTtlExpirationDefaults) - .build(); -``` - -Or, alternatively: - -*Global, dynamically computed per-cache entry Duration TTL expiration timeout* - -```java -RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(MyCustomTtlFunction.INSTANCE); - -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(defaults) - .build(); -``` - -Of course, you can combine both global and per-cache configuration using: - -*Global fixed Duration TTL expiration timeout* - -```java -RedisCacheConfiguration predefined = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(MyCustomTtlFunction.INSTANCE); - -Map initialCaches = Collections.singletonMap("predefined", predefined); - -RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) - .cacheDefaults(fiveMinuteTtlExpirationDefaults) - .withInitialCacheConfigurations(initialCaches) - .build(); -``` - -### Time-To-Idle (TTI) Expiration - -Redis itself does not support the concept of true, time-to-idle (TTI) expiration. -Still, using Spring Data Redis's Cache implementation, it is possible to achieve time-to-idle (TTI) expiration-like behavior. - -The configuration of TTI in Spring Data Redis's Cache implementation must be explicitly enabled, that is, is opt-in. -Additionally, you must also provide TTL configuration using either a fixed `Duration` or a custom implementation of the `TtlFunction` interface as described above in [Redis Cache Expiration](#redis-cache-expiration). - -For example: - -```java -@Configuration -@EnableCaching -class RedisConfiguration { - - @Bean - RedisConnectionFactory redisConnectionFactory() { - // ... - } - - @Bean - RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { - - RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofMinutes(5)) - .enableTimeToIdle(); - - return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(defaults) - .build(); - } -} -``` - -Because Redis servers do not implement a proper notion of TTI, then TTI can only be achieved with Redis commands accepting expiration options. -In Redis, the "expiration" is technically a time-to-live (TTL) policy. -However, TTL expiration can be passed when reading the value of a key thereby effectively resetting the TTL expiration timeout, as is now the case in Spring Data Redis's `Cache.get(key)` operation. - -`RedisCache.get(key)` is implemented by calling the Redis `GETEX` command. - -:::danger -The Redis [`GETEX`](https://redis.io/commands/getex) command is only available in Redis version `6.2.0` and later. -Therefore, if you are not using Redis `6.2.0` or later, then it is not possible to use Spring Data Redis's TTI expiration. -A command execution exception will be thrown if you enable TTI against an incompatible Redis (server) version. -No attempt is made to determine if the Redis server version is correct and supports the `GETEX` command. -::: - -:::danger -In order to achieve true time-to-idle (TTI) expiration-like behavior in your Spring Data Redis application, then an entry must be consistently accessed with (TTL) expiration on every read or write operation. -There are no exceptions to this rule. -If you are mixing and matching different data access patterns across your Spring Data Redis application (for example: caching, invoking operations using `RedisTemplate` and possibly, or especially when using Spring Data Repository CRUD operations), then accessing an entry may not necessarily prevent the entry from expiring if TTL expiration was set. -For example, an entry maybe "put" in (written to) the cache during a `@Cacheable` service method invocation with a TTL expiration (i.e. `SET `) and later read using a Spring Data Redis Repository before the expiration timeout (using `GET` without expiration options). -A simple `GET` without specifying expiration options will not reset the TTL expiration timeout on an entry. -Therefore, the entry may expire before the next data access operation, even though it was just read. -Since this cannot be enforced in the Redis server, then it is the responsibility of your application to consistently access an entry when time-to-idle expiration is configured, in and outside of caching, where appropriate. -::: diff --git a/docs/src/content/docs/redis/redis-repositories/cdi-integration.md b/docs/src/content/docs/redis/redis-repositories/cdi-integration.md deleted file mode 100644 index 95e5252d3..000000000 --- a/docs/src/content/docs/redis/redis-repositories/cdi-integration.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: CDI Integration -description: CDI Integration documentation ---- - -Instances of the repository interfaces are usually created by a container, for which Spring is the most natural choice when working with Spring Data. -Spring offers sophisticated support for creating bean instances. -Spring Data Redis ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. -The extension is part of the JAR, so, to activate it, drop the Spring Data Redis JAR into your classpath. - -You can then set up the infrastructure by implementing a CDI Producer for the `org.springframework.data.redis.connection.RedisConnectionFactory` and `org.springframework.data.redis.core.RedisOperations`, as shown in the following example: - -```java -class RedisOperationsProducer { - @Produces - RedisConnectionFactory redisConnectionFactory() { - - LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration()); - connectionFactory.afterPropertiesSet(); - connectionFactory.start(); - - return connectionFactory; - } - - void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception { - - if (redisConnectionFactory instanceof DisposableBean) { - ((DisposableBean) redisConnectionFactory).destroy(); - } - } - - @Produces - @ApplicationScoped - RedisOperations redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) { - - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - template.afterPropertiesSet(); - - return template; - } - -} -``` - -The necessary setup can vary, depending on your JavaEE environment. - -The Spring Data Redis CDI extension picks up all available repositories as CDI beans and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. -Thus, obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property, as shown in the following example: - -```java -class RepositoryClient { - - @Inject - PersonRepository repository; - - public void businessMethod() { - List people = repository.findAll(); - } -} -``` - -A Redis Repository requires `org.springframework.data.redis.core.RedisKeyValueAdapter` and `org.springframework.data.redis.core.RedisKeyValueTemplate` instances. -These beans are created and managed by the Spring Data CDI extension if no provided beans are found. -You can, however, supply your own beans to configure the specific properties of `org.springframework.data.redis.core.RedisKeyValueAdapter` and `org.springframework.data.redis.core.RedisKeyValueTemplate`. diff --git a/docs/src/content/docs/redis/scripting.mdx b/docs/src/content/docs/redis/scripting.mdx deleted file mode 100644 index 501169a8a..000000000 --- a/docs/src/content/docs/redis/scripting.mdx +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Scripting -description: Scripting documentation ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -Redis versions 2.6 and higher provide support for running Lua scripts through the [eval](https://redis.io/commands/eval) and [evalsha](https://redis.io/commands/evalsha) commands. Spring Data Redis provides a high-level abstraction for running scripts that handles serialization and automatically uses the Redis script cache. - -Scripts can be run by calling the `execute` methods of `RedisTemplate` and `ReactiveRedisTemplate`. Both use a configurable `org.springframework.data.redis.core.script.ScriptExecutor` (or `org.springframework.data.redis.core.script.ReactiveScriptExecutor`) to run the provided script. By default, the `org.springframework.data.redis.core.script.ScriptExecutor` (or `org.springframework.data.redis.core.script.ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. - -The default `org.springframework.data.redis.core.script.ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Redis script cache. - -The following example runs a common "check-and-set" scenario by using a Lua script. This is an ideal use case for a Redis script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. - -```java -@Bean -public RedisScript script() { - - ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")); - return RedisScript.of(scriptSource, Boolean.class); -} -``` - - - - -```java -public class Example { - - @Autowired - RedisOperations redisOperations; - - @Autowired - RedisScript script; - - public boolean checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, List.of("key"), expectedValue, newValue); - } -} -``` - - - - -```java -public class Example { - - @Autowired - ReactiveRedisOperations redisOperations; - - @Autowired - RedisScript script; - - public Flux checkAndSet(String expectedValue, String newValue) { - return redisOperations.execute(script, List.of("key"), expectedValue, newValue); - } -} -``` - - - - -```lua --- checkandset.lua -local current = redis.call('GET', KEYS[1]) -if current == ARGV[1] - then redis.call('SET', KEYS[1], ARGV[2]) - return true -end -return false -``` - -The preceding code configures a `org.springframework.data.redis.core.script.RedisScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). - -:::tip -It is ideal to configure a single instance of `DefaultRedisScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. -::: - -The `checkAndSet` method above then runs the scripts. Scripts can be run within a `org.springframework.data.redis.core.SessionCallback` as part of a transaction or pipeline. See "[Redis Transactions](/redis/transactions)" and "[Pipelining](/redis/pipelining)" for more information. - -The scripting support provided by Spring Data Redis also lets you schedule Redis scripts for periodic running by using the Spring Task and Scheduler abstractions. See the [Spring Framework](https://spring.io/projects/spring-framework/) documentation for more details. diff --git a/docs/src/content/docs/redis/support-classes.mdx b/docs/src/content/docs/redis/support-classes.mdx deleted file mode 100644 index 335313172..000000000 --- a/docs/src/content/docs/redis/support-classes.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Support Classes -description: Support Classes documentation ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -Package `org.springframework.data.redis.support` offers various reusable components that rely on Redis as a backing store. -Currently, the package contains various JDK-based interface implementations on top of Redis, such as [atomic](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html) counters and JDK [Collections](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html). - -:::note -`org.springframework.data.redis.support.collections.RedisList` is forward-compatible with Java 21 `SequencedCollection`. -::: - -The atomic counters make it easy to wrap Redis key incrementation while the collections allow easy management of Redis keys with minimal storage exposure or API leakage. -In particular, the `org.springframework.data.redis.support.collections.RedisSet` and `org.springframework.data.redis.support.collections.RedisZSet` interfaces offer easy access to the set operations supported by Redis, such as `intersection` and `union`. `org.springframework.data.redis.support.collections.RedisList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Redis, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. -The following example shows the configuration for a bean that uses a `org.springframework.data.redis.support.collections.RedisList`: - - - - -```java -@Configuration -class MyConfig { - - // … - - @Bean - RedisList stringRedisTemplate(RedisTemplate redisTemplate) { - return new DefaultRedisList<>(template, "queue-key"); - } -} -``` - - - - -```xml - - - - - - - - - -``` - - - - -The following example shows a Java configuration example for a `Deque`: - -```java -public class AnotherExample { - - // injected - private Deque queue; - - public void addTag(String tag) { - queue.push(tag); - } -} -``` - -As shown in the preceding example, the consuming code is decoupled from the actual storage implementation. -In fact, there is no indication that Redis is used underneath. -This makes moving from development to production environments transparent and highly increases testability (the Redis implementation can be replaced with an in-memory one). diff --git a/docs/src/content/docs/redis/template.mdx b/docs/src/content/docs/redis/template.mdx deleted file mode 100644 index 22fa94215..000000000 --- a/docs/src/content/docs/redis/template.mdx +++ /dev/null @@ -1,341 +0,0 @@ ---- -title: Working with Objects through RedisTemplate -description: Template documentation ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -Most users are likely to use `org.springframework.data.redis.core.RedisTemplate` and its corresponding package, `org.springframework.data.redis.core` or its reactive variant `org.springframework.data.redis.core.ReactiveRedisTemplate`. -The template is, in fact, the central class of the Redis module, due to its rich feature set. -The template offers a high-level abstraction for Redis interactions. -While `[Reactive]RedisConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. - -The `org.springframework.data.redis.core.RedisTemplate` class implements the `org.springframework.data.redis.core.RedisOperations` interface and its reactive variant `org.springframework.data.redis.core.ReactiveRedisTemplate` implements `org.springframework.data.redis.core.ReactiveRedisOperations`. - -:::note -The preferred way to reference operations on a `[Reactive]RedisTemplate` instance is through the -`[Reactive]RedisOperations` interface. -::: - -Moreover, the template provides operations views (following the grouping from the Redis command [reference](https://redis.io/commands)) that offer rich, generified interfaces for working against a certain type or certain key (through the `KeyBound` interfaces) as described in the following table: - -
-Operational views - - - - -| Interface | Description | -|-----------|-------------| -| *Key Type Operations* | | -| `org.springframework.data.redis.core.GeoOperations` | Redis geospatial operations, such as `GEOADD`, `GEORADIUS`,... | -| `org.springframework.data.redis.core.HashOperations` | Redis hash operations | -| `org.springframework.data.redis.core.HyperLogLogOperations` | Redis HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... | -| `org.springframework.data.redis.core.ListOperations` | Redis list operations | -| `org.springframework.data.redis.core.SetOperations` | Redis set operations | -| `org.springframework.data.redis.core.ValueOperations` | Redis string (or value) operations | -| `org.springframework.data.redis.core.ZSetOperations` | Redis zset (or sorted set) operations | -| *Key Bound Operations* | | -| `org.springframework.data.redis.core.BoundGeoOperations` | Redis key bound geospatial operations | -| `org.springframework.data.redis.core.BoundHashOperations` | Redis hash key bound operations | -| `org.springframework.data.redis.core.BoundKeyOperations` | Redis key bound operations | -| `org.springframework.data.redis.core.BoundListOperations` | Redis list key bound operations | -| `org.springframework.data.redis.core.BoundSetOperations` | Redis set key bound operations | -| `org.springframework.data.redis.core.BoundValueOperations` | Redis string (or value) key bound operations | -| `org.springframework.data.redis.core.BoundZSetOperations` | Redis zset (or sorted set) key bound operations | - - - - -| Interface | Description | -|-----------|-------------| -| *Key Type Operations* | | -| `org.springframework.data.redis.core.ReactiveGeoOperations` | Redis geospatial operations such as `GEOADD`, `GEORADIUS`, and others | -| `org.springframework.data.redis.core.ReactiveHashOperations` | Redis hash operations | -| `org.springframework.data.redis.core.ReactiveHyperLogLogOperations` | Redis HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) | -| `org.springframework.data.redis.core.ReactiveListOperations` | Redis list operations | -| `org.springframework.data.redis.core.ReactiveSetOperations` | Redis set operations | -| `org.springframework.data.redis.core.ReactiveValueOperations` | Redis string (or value) operations | -| `org.springframework.data.redis.core.ReactiveZSetOperations` | Redis zset (or sorted set) operations | - - - - -
- -Once configured, the template is thread-safe and can be reused across multiple instances. - -`RedisTemplate` uses a Java-based serializer for most of its operations. -This means that any object written or read by the template is serialized and deserialized through Java. - -You can change the serialization mechanism on the template, and the Redis module offers several implementations, which are available in the `org.springframework.data.redis.serializer` package. -See [Serializers](#serializers) for more information. -You can also set any of the serializers to null and use RedisTemplate with raw byte arrays by setting the `enableDefaultSerializer` property to `false`. -Note that the template requires all keys to be non-null. -However, values can be null as long as the underlying serializer accepts them. -Read the Javadoc of each serializer for more information. - -For cases where you need a certain template view, declare the view as a dependency and inject the template. -The container automatically performs the conversion, eliminating the `opsFor[X]` calls, as shown in the following example: - -*Configuring Template API* - - - - -```java -@Configuration -class MyConfig { - - @Bean - LettuceConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - return template; - } -} -``` - - - - -```java -@Configuration -class MyConfig { - - @Bean - LettuceConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - ReactiveRedisTemplate ReactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) { - return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string()); - } -} -``` - - - - -```xml - - - - - - - ... - - -``` - - - - -*Pushing an item to a List using `[Reactive]RedisTemplate`* - - - - -```java -public class Example { - - // inject the actual operations - @Autowired - private RedisOperations operations; - - // inject the template as ListOperations - @Resource(name="redisTemplate") - private ListOperations listOps; - - public void addLink(String userId, URL url) { - listOps.leftPush(userId, url.toExternalForm()); - } -} -``` - - - - -```java -public class Example { - - // inject the actual template - @Autowired - private ReactiveRedisOperations operations; - - public Mono addLink(String userId, URL url) { - return operations.opsForList().leftPush(userId, url.toExternalForm()); - } -} -``` - - - - -## String-focused Convenience Classes - -Since it is quite common for the keys and values stored in Redis to be `java.lang.String`, the Redis modules provides two extensions to `RedisConnection` and `RedisTemplate`, respectively the `StringRedisConnection` (and its `DefaultStringRedisConnection` implementation) and `StringRedisTemplate` as a convenient one-stop solution for intensive String operations. -In addition to being bound to `String` keys, the template and the connection use the `StringRedisSerializer` underneath, which means the stored keys and values are human-readable (assuming the same encoding is used both in Redis and your code). -The following listings show an example: - - - - -```java -@Configuration -class RedisConfiguration { - - @Bean - LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - - StringRedisTemplate template = new StringRedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); - return template; - } -} -``` - - - - -```java -@Configuration -class RedisConfiguration { - - @Bean - LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); - } - - @Bean - ReactiveStringRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) { - return new ReactiveStringRedisTemplate<>(factory); - } -} -``` - - - - -```xml - - - - - - - - -``` - - - - - - - -```java -public class Example { - - @Autowired - private StringRedisTemplate redisTemplate; - - public void addLink(String userId, URL url) { - redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); - } -} -``` - - - - -```java -public class Example { - - @Autowired - private ReactiveStringRedisTemplate redisTemplate; - - public Mono addLink(String userId, URL url) { - return redisTemplate.opsForList().leftPush(userId, url.toExternalForm()); - } -} -``` - - - - -As with the other Spring templates, `RedisTemplate` and `StringRedisTemplate` let you talk directly to Redis through the `RedisCallback` interface. -This feature gives complete control to you, as it talks directly to the `RedisConnection`. -Note that the callback receives an instance of `StringRedisConnection` when a `StringRedisTemplate` is used. -The following example shows how to use the `RedisCallback` interface: - -```java -public void useCallback() { - - redisOperations.execute(new RedisCallback() { - public Object doInRedis(RedisConnection connection) throws DataAccessException { - Long size = connection.dbSize(); - // Can cast to StringRedisConnection if using a StringRedisTemplate - ((StringRedisConnection)connection).set("key", "value"); - } - }); -} -``` - -## Serializers - -From the framework perspective, the data stored in Redis is only bytes. -While Redis itself supports various types, for the most part, these refer to the way the data is stored rather than what it represents. -It is up to the user to decide whether the information gets translated into strings or any other objects. - -In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Redis in the `org.springframework.data.redis.serializer` package. - -This package contains two types of serializers that, as the name implies, take care of the serialization process: - -* Two-way serializers based on `org.springframework.data.redis.serializer.RedisSerializer`. -* Element readers and writers that use `RedisElementReader` and ``RedisElementWriter``. - -The main difference between these variants is that `RedisSerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. - -Multiple implementations are available (including two that have been already mentioned in this documentation): - -* `org.springframework.data.redis.serializer.JdkSerializationRedisSerializer`, which is used by default for `org.springframework.data.redis.cache.RedisCache` and `org.springframework.data.redis.core.RedisTemplate`. -* the `StringRedisSerializer`. - -However, one can use `OxmSerializer` for Object/XML mapping through Spring [OXM](https://docs.spring.io/spring-framework/reference/data-access.html#oxm) support or `org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer` or `org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer` for storing data in [JSON](https://en.wikipedia.org/wiki/JSON) format. - -Do note that the storage format is not limited only to values. -It can be used for keys, values, or hashes without any restrictions. - -:::danger -By default, `org.springframework.data.redis.cache.RedisCache` and `org.springframework.data.redis.core.RedisTemplate` are configured to use Java native serialization. -Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. -Manipulated input could lead to unwanted code being run in the application during the deserialization step. -As a consequence, do not use serialization in untrusted environments. -In general, we strongly recommend any other message format (such as JSON) instead. - -If you are concerned about security vulnerabilities due to Java serialization, consider the general-purpose serialization filter mechanism at the core JVM level: - -* [Filter Incoming Serialization Data](https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html). -* [JEP 290](https://openjdk.org/jeps/290). -* [OWASP: Deserialization of untrusted data](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data). -::: diff --git a/docs/src/content/docs/redis/transactions.md b/docs/src/content/docs/redis/transactions.md deleted file mode 100644 index d0df01e23..000000000 --- a/docs/src/content/docs/redis/transactions.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: Redis Transactions -description: Transactions documentation ---- - -Redis provides support for [transactions](https://redis.io/topics/transactions) through the `multi`, `exec`, and `discard` commands. -These operations are available on `org.springframework.data.redis.core.RedisTemplate`. -However, `RedisTemplate` is not guaranteed to run all the operations in the transaction with the same connection. - -Spring Data Redis provides the `org.springframework.data.redis.core.SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Redis transactions.The following example uses the `multi` method: - -```java -//execute a transaction -List txResults = redisOperations.execute(new SessionCallback>() { - public List execute(RedisOperations operations) throws DataAccessException { - operations.multi(); - operations.opsForSet().add("key", "value1"); - - // This will contain the results of all operations in the transaction - return operations.exec(); - } -}); -System.out.println("Number of items added to set: " + txResults.get(0)); -``` - -`RedisTemplate` uses its value, hash key, and hash value serializers to deserialize all results of `exec` before returning. -There is an additional `exec` method that lets you pass a custom serializer for transaction results. - -It is worth mentioning that in case between `multi()` and `exec()` an exception happens (e.g. a timeout exception in case Redis does not respond within the timeout) then the connection may get stuck in a transactional state. -To prevent such a situation need have to discard the transactional state to clear the connection: - -```java -List txResults = redisOperations.execute(new SessionCallback>() { - public List execute(RedisOperations operations) throws DataAccessException { - boolean transactionStateIsActive = true; - try { - operations.multi(); - operations.opsForSet().add("key", "value1"); - - // This will contain the results of all operations in the transaction - return operations.exec(); - } catch (RuntimeException e) { - operations.discard(); - throw e; - } - } -}); -``` - -## `@Transactional` Support - -By default, `RedisTemplate` does not participate in managed Spring transactions. -If you want `RedisTemplate` to make use of Redis transaction when using `@Transactional` or `TransactionTemplate`, you need to be explicitly enable transaction support for each `RedisTemplate` by setting `setEnableTransactionSupport(true)`. -Enabling transaction support binds `RedisConnection` to the current transaction backed by a `ThreadLocal`. -If the transaction finishes without errors, the Redis transaction gets commited with `EXEC`, otherwise rolled back with `DISCARD`. -Redis transactions are batch-oriented. -Commands issued during an ongoing transaction are queued and only applied when committing the transaction. - -Spring Data Redis distinguishes between read-only and write commands in an ongoing transaction. -Read-only commands, such as `KEYS`, are piped to a fresh (non-thread-bound) `RedisConnection` to allow reads. -Write commands are queued by `RedisTemplate` and applied upon commit. - -The following example shows how to configure transaction management: - -*Example 1. Configuration enabling Transaction Management* - -```java -@Configuration -@EnableTransactionManagement // (1) -public class RedisTxContextConfiguration { - - @Bean - public StringRedisTemplate redisTemplate() { - StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory()); - // explicitly enable transaction support - template.setEnableTransactionSupport(true); // (2) - return template; - } - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - // jedis || Lettuce - } - - @Bean - public PlatformTransactionManager transactionManager() throws SQLException { - return new DataSourceTransactionManager(dataSource()); // (3) - } - - @Bean - public DataSource dataSource() throws SQLException { - // ... - } -} -``` -```text -1. Configures a Spring Context to enable [declarative transaction management](https://docs.spring.io/spring-framework/reference/data-access.html#transaction-declarative). -2. Configures `RedisTemplate` to participate in transactions by binding connections to the current thread. -3. Transaction management requires a `PlatformTransactionManager`. -Spring Data Redis does not ship with a `PlatformTransactionManager` implementation. -Assuming your application uses JDBC, Spring Data Redis can participate in transactions by using existing transaction managers. -``` - -The following examples each demonstrate a usage constraint: - -*Example 2. Usage Constraints* - -```java -// must be performed on thread-bound connection -template.opsForValue().set("thing1", "thing2"); - -// read operation must be run on a free (not transaction-aware) connection -template.keys("*"); - -// returns null as values set within a transaction are not visible -template.opsForValue().get("thing1"); -``` diff --git a/docs/src/content/docs/repositories.md b/docs/src/content/docs/repositories.md index 1fc06f311..9648c62cc 100644 --- a/docs/src/content/docs/repositories.md +++ b/docs/src/content/docs/repositories.md @@ -1,15 +1,15 @@ --- -title: Redis Repositories Overview -description: Redis Repositories with Spring Data +title: Valkey Repositories Overview +description: Valkey Repositories with Spring Data --- -This chapter explains the basic foundations of Spring Data repositories and Redis specifics. -Before continuing to the Redis specifics, make sure you have a sound understanding of the basic concepts. +This chapter explains the basic foundations of Spring Data repositories and Valkey specifics. +Before continuing to the Valkey specifics, make sure you have a sound understanding of the basic concepts. The goal of the Spring Data repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores. -Working with Redis Repositories lets you seamlessly convert and store domain objects in Redis Hashes, apply custom mapping strategies, and use secondary indexes. +Working with Valkey Repositories lets you seamlessly convert and store domain objects in Valkey Hashes, apply custom mapping strategies, and use secondary indexes. :::note[Important] -Redis Repositories require at least Redis Server version 2.8.0 and do not work with transactions. Make sure to use a `RedisTemplate` with [disabled transaction support](/redis/transactions#tx.spring). +Valkey Repositories require at least Valkey Server version 2.8.0 and do not work with transactions. Make sure to use a `ValkeyTemplate` with [disabled transaction support](/valkey/transactions#tx.spring). ::: diff --git a/docs/src/content/docs/repositories/core-extensions.md b/docs/src/content/docs/repositories/core-extensions.md index 1ec1f3c9d..508d13604 100644 --- a/docs/src/content/docs/repositories/core-extensions.md +++ b/docs/src/content/docs/repositories/core-extensions.md @@ -5,4 +5,4 @@ description: Core Extensions documentation ## Querydsl -Spring Data Redis does not support Querydsl. +Spring Data Valkey does not support Querydsl. diff --git a/docs/src/content/docs/valkey.md b/docs/src/content/docs/valkey.md new file mode 100644 index 000000000..dbf470727 --- /dev/null +++ b/docs/src/content/docs/valkey.md @@ -0,0 +1,42 @@ +--- +title: Valkey Overview +description: Spring Data Valkey support and connectivity +--- + +One of the key-value stores supported by Spring Data is [Valkey](https://valkey.io). +To quote the Valkey project home page: + +> Valkey is an advanced key-value store. +> It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. +> All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. +> Valkey supports different kind of sorting abilities. + +Spring Data Valkey provides easy configuration and access to Valkey from Spring applications. +It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns. + +Spring Data support for Valkey contains a wide range of features: + +* [`ValkeyTemplate` and `ReactiveValkeyTemplate` helper class](/valkey/template) that increases productivity when performing common Valkey operations. +Includes integrated serialization between objects and values. +* Exception translation into Spring's portable Data Access Exception hierarchy. +* Automatic implementation of [Repository interfaces](/repositories), including support for custom query methods. +* Feature-rich [Object Mapping](/valkey/valkey-repositories/mapping) integrated with Spring's Conversion Service. +* Annotation-based mapping metadata that is extensible to support other metadata formats. +* [Transactions](/valkey/transactions) and [Pipelining](/valkey/pipelining). +* [Valkey Cache](/valkey/valkey-cache) integration through Spring's Cache abstraction. +* [Valkey Pub/Sub Messaging](/valkey/pubsub) and [Valkey Stream](/valkey/valkey-streams) Listeners. +* [Valkey Collection Implementations](/valkey/support-classes) for Java such as `ValkeyList` or `ValkeySet`. + +## Why Spring Data Valkey? + +The Spring Framework is the leading full-stack Java/JEE application framework. +It provides a lightweight container and a non-invasive programming model enabled by the use of dependency injection, AOP, and portable service abstractions. + +[NoSQL](https://en.wikipedia.org/wiki/NoSQL) storage systems provide an alternative to classical RDBMS for horizontal scalability and speed. +In terms of implementation, key-value stores represent one of the largest (and oldest) members in the NoSQL space. + +The Spring Data Valkey (SDR) framework makes it easy to write Spring applications that use the Valkey key-value store by eliminating the redundant tasks and boilerplate code required for interacting with the store through Spring's excellent infrastructure support. + +## Valkey Support High-level View + +The Valkey support provides several components.For most tasks, the high-level abstractions and support services are the best choice.Note that, at any point, you can move between layers.For example, you can get a low-level connection (or even the native library) to communicate directly with Valkey. diff --git a/docs/src/content/docs/redis/cluster.md b/docs/src/content/docs/valkey/cluster.md similarity index 64% rename from docs/src/content/docs/redis/cluster.md rename to docs/src/content/docs/valkey/cluster.md index 9634f5100..757b011b6 100644 --- a/docs/src/content/docs/redis/cluster.md +++ b/docs/src/content/docs/valkey/cluster.md @@ -1,25 +1,25 @@ --- -title: Redis Cluster -description: Redis Cluster documentation +title: Valkey Cluster +description: Valkey Cluster documentation --- -Working with [Redis Cluster](https://redis.io/topics/cluster-spec) requires Redis Server version 3.0+. -See the [Cluster Tutorial](https://redis.io/topics/cluster-tutorial) for more information. +Working with [Valkey Cluster](https://valkey.io/topics/cluster-spec) requires Valkey Server version 3.0+. +See the [Cluster Tutorial](https://valkey.io/topics/cluster-tutorial) for more information. :::note -When using [Redis Repositories](/repositories) with Redis Cluster, make yourself familiar with how to [run Redis Repositories on a Cluster](/redis/redis-repositories/cluster). +When using [Valkey Repositories](/repositories) with Valkey Cluster, make yourself familiar with how to [run Valkey Repositories on a Cluster](/valkey/valkey-repositories/cluster). ::: :::caution -Do not rely on keyspace events when using Redis Cluster as keyspace events are not replicated across shards. +Do not rely on keyspace events when using Valkey Cluster as keyspace events are not replicated across shards. ::: -Pub/Sub [subscribes to a random cluster node](https://github.com/spring-projects/spring-data-redis/issues/1111) which only receives keyspace events from a single shard. -Use single-node Redis to avoid keyspace event loss. +Pub/Sub [subscribes to a random cluster node](https://github.com/valkey-io/spring-data-valkey/issues/1111) which only receives keyspace events from a single shard. +Use single-node Valkey to avoid keyspace event loss. -## Working With Redis Cluster Connection +## Working With Valkey Cluster Connection -Redis Cluster behaves differently from single-node Redis or even a Sentinel-monitored master-replica environment. +Valkey Cluster behaves differently from single-node Valkey or even a Sentinel-monitored master-replica environment. This is because the automatic sharding maps a key to one of `16384` slots, which are distributed across the nodes. Therefore, commands that involve more than one key must assert all keys map to the exact same slot to avoid cross-slot errors. A single cluster node serves only a dedicated set of keys. @@ -28,18 +28,18 @@ As a simple example, consider the `KEYS` command. When issued to a server in a cluster environment, it returns only the keys served by the node the request is sent to and not necessarily all keys within the cluster. So, to get all keys in a cluster environment, you must read the keys from all the known master nodes. -While redirects for specific keys to the corresponding slot-serving node are handled by the driver libraries, higher-level functions, such as collecting information across nodes or sending commands to all nodes in the cluster, are covered by `RedisClusterConnection`. +While redirects for specific keys to the corresponding slot-serving node are handled by the driver libraries, higher-level functions, such as collecting information across nodes or sending commands to all nodes in the cluster, are covered by `ValkeyClusterConnection`. Picking up the keys example from earlier, this means that the `keys(pattern)` method picks up every master node in the cluster and simultaneously runs the `KEYS` command on every master node while picking up the results and returning the cumulated set of keys. -To just request the keys of a single node `RedisClusterConnection` provides overloads for those methods (for example, `keys(node, pattern)`). +To just request the keys of a single node `ValkeyClusterConnection` provides overloads for those methods (for example, `keys(node, pattern)`). -A `RedisClusterNode` can be obtained from `RedisClusterConnection.clusterGetNodes` or it can be constructed by using either the host and the port or the node Id. +A `ValkeyClusterNode` can be obtained from `ValkeyClusterConnection.clusterGetNodes` or it can be constructed by using either the host and the port or the node Id. The following example shows a set of commands being run across the cluster: *Example 1. Sample of Running Commands Across the Cluster* ```text -redis-cli@127.0.0.1:7379 > cluster nodes +valkey-cli@127.0.0.1:7379 > cluster nodes 6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 // (1) 7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-20252 // (2) @@ -48,7 +48,7 @@ b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected / ``` ```java -RedisClusterConnection connection = connectionFactory.getClusterConnection(); +ValkeyClusterConnection connection = connectionFactory.getClusterConnection(); connection.set("thing1", value); // (5) connection.set("thing2", value); // (6) @@ -75,7 +75,7 @@ connection.keys(NODE_7382, "*"); / ``` When all keys map to the same slot, the native driver library automatically serves cross-slot requests, such as `MGET`. -However, once this is not the case, `RedisClusterConnection` runs multiple parallel `GET` commands against the slot-serving nodes and again returns an accumulated result. +However, once this is not the case, `ValkeyClusterConnection` runs multiple parallel `GET` commands against the slot-serving nodes and again returns an accumulated result. This is less performant than the single-slot approach and, therefore, should be used with care. If in doubt, consider pinning keys to the same slot by providing a prefix in curly brackets, such as `{my-prefix}.thing1` and `{my-prefix}.thing2`, which will both map to the same slot number. The following example shows cross-slot request handling: @@ -83,14 +83,14 @@ The following example shows cross-slot request handling: *Example 2. Sample of Cross-Slot Request Handling* ```text -redis-cli@127.0.0.1:7379 > cluster nodes +valkey-cli@127.0.0.1:7379 > cluster nodes 6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 // (1) 7bb... ``` ```java -RedisClusterConnection connection = connectionFactory.getClusterConnection(); +ValkeyClusterConnection connection = connectionFactory.getClusterConnection(); connection.set("thing1", value); // slot: 12182 connection.set("{thing1}.thing2", value); // slot: 12182 @@ -109,30 +109,30 @@ connection.mGet("thing1", "thing2"); / ``` :::tip -The preceding examples demonstrate the general strategy followed by Spring Data Redis. +The preceding examples demonstrate the general strategy followed by Spring Data Valkey. ::: Be aware that some operations might require loading huge amounts of data into memory to compute the desired command. Additionally, not all cross-slot requests can safely be ported to multiple single slot requests and error if misused (for example, `PFCOUNT`). -## Working with `RedisTemplate` and `ClusterOperations` +## Working with `ValkeyTemplate` and `ClusterOperations` -See the [Working with Objects through RedisTemplate](/redis/template) section for information about the general purpose, configuration, and usage of `RedisTemplate`. +See the [Working with Objects through ValkeyTemplate](/valkey/template) section for information about the general purpose, configuration, and usage of `ValkeyTemplate`. :::caution -Be careful when setting up `RedisTemplate#keySerializer` using any of the JSON `RedisSerializers`, as changing JSON structure has immediate influence on hash slot calculation. +Be careful when setting up `ValkeyTemplate#keySerializer` using any of the JSON `ValkeySerializers`, as changing JSON structure has immediate influence on hash slot calculation. ::: -`RedisTemplate` provides access to cluster-specific operations through the `ClusterOperations` interface, which can be obtained from `RedisTemplate.opsForCluster()`. +`ValkeyTemplate` provides access to cluster-specific operations through the `ClusterOperations` interface, which can be obtained from `ValkeyTemplate.opsForCluster()`. This lets you explicitly run commands on a single node within the cluster while retaining the serialization and deserialization features configured for the template. It also provides administrative commands (such as `CLUSTER MEET`) or more high-level operations (for example, resharding). -The following example shows how to access `RedisClusterConnection` with `RedisTemplate`: +The following example shows how to access `ValkeyClusterConnection` with `ValkeyTemplate`: -*Example 3. Accessing `RedisClusterConnection` with `RedisTemplate`* +*Example 3. Accessing `ValkeyClusterConnection` with `ValkeyTemplate`* ```java -ClusterOperations clusterOps = redisTemplate.opsForCluster(); +ClusterOperations clusterOps = valkeyTemplate.opsForCluster(); clusterOps.shutdown(NODE_7379); // (1) ``` ```text @@ -140,5 +140,5 @@ clusterOps.shutdown(NODE_7379); // ``` :::note -Redis Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Valkey Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. ::: diff --git a/docs/src/content/docs/valkey/connection-modes.md b/docs/src/content/docs/valkey/connection-modes.md new file mode 100644 index 000000000..dda57a8c5 --- /dev/null +++ b/docs/src/content/docs/valkey/connection-modes.md @@ -0,0 +1,171 @@ +--- +title: Connection Modes +description: Connection Modes documentation +--- + +Valkey can be operated in various setups. +Each mode of operation requires specific configuration that is explained in the following sections. + +## Valkey Standalone + +The easiest way to get started is by using Valkey Standalone with a single Valkey server, + +Configure `io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory` or `io.valkey.springframework.data.connection.jedis.JedisConnectionFactory`, as shown in the following example: + +```java +@Configuration +class ValkeyStandaloneConfiguration { + + /** + * Lettuce + */ + @Bean + public ValkeyConnectionFactory lettuceConnectionFactory() { + return new LettuceConnectionFactory(new ValkeyStandaloneConfiguration("server", 6379)); + } + + /** + * Jedis + */ + @Bean + public ValkeyConnectionFactory jedisConnectionFactory() { + return new JedisConnectionFactory(new ValkeyStandaloneConfiguration("server", 6379)); + } +} +``` + +## Write to Master, Read from Replica + +The Valkey Master/Replica setup -- without automatic failover (for automatic failover see: [Sentinel](/valkey/connection-modes#valkey-sentinel)) -- not only allows data to be safely stored at more nodes. +It also allows, by using [Lettuce](/valkey/drivers#configuring-the-lettuce-connector), reading data from replicas while pushing writes to the master. +You can set the read/write strategy to be used by using `LettuceClientConfiguration`, as shown in the following example: + +```java +@Configuration +class WriteToMasterReadFromReplicaConfiguration { + + @Bean + public LettuceConnectionFactory valkeyConnectionFactory() { + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .readFrom(REPLICA_PREFERRED) + .build(); + + ValkeyStandaloneConfiguration serverConfig = new ValkeyStandaloneConfiguration("server", 6379); + + return new LettuceConnectionFactory(serverConfig, clientConfig); + } +} +``` + +:::tip +For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `io.valkey.springframework.data.connection.ValkeyStaticMasterReplicaConfiguration` instead of `io.valkey.springframework.data.connection.ValkeyStandaloneConfiguration`. Please note that `ValkeyStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. +::: + +## Valkey Sentinel + +For dealing with high-availability Valkey, Spring Data Valkey has support for [Valkey Sentinel](https://valkey.io/topics/sentinel), using `io.valkey.springframework.data.connection.ValkeySentinelConfiguration`, as shown in the following example: + +```java +/** + * Lettuce + */ +@Bean +public ValkeyConnectionFactory lettuceConnectionFactory() { + ValkeySentinelConfiguration sentinelConfig = new ValkeySentinelConfiguration() + .master("mymaster") + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + return new LettuceConnectionFactory(sentinelConfig); +} + +/** + * Jedis + */ +@Bean +public ValkeyConnectionFactory jedisConnectionFactory() { + ValkeySentinelConfiguration sentinelConfig = new ValkeySentinelConfiguration() + .master("mymaster") + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + return new JedisConnectionFactory(sentinelConfig); +} +``` + +:::tip +`ValkeySentinelConfiguration` can also be defined through `ValkeySentinelConfiguration.of(PropertySource)`, which lets you pick up the following properties: + +*Configuration Properties* + +* `spring.valkey.sentinel.master`: name of the master node. +* `spring.valkey.sentinel.nodes`: Comma delimited list of host:port pairs. +* `spring.valkey.sentinel.username`: The username to apply when authenticating with Valkey Sentinel (requires Valkey 6) +* `spring.valkey.sentinel.password`: The password to apply when authenticating with Valkey Sentinel +* `spring.valkey.sentinel.dataNode.username`: The username to apply when authenticating with Valkey Data Node +* `spring.valkey.sentinel.dataNode.password`: The password to apply when authenticating with Valkey Data Node +* `spring.valkey.sentinel.dataNode.database`: The database index to apply when authenticating with Valkey Data Node +::: + +Sometimes, direct interaction with one of the Sentinels is required. Using `ValkeyConnectionFactory.getSentinelConnection()` or `ValkeyConnection.getSentinelCommands()` gives you access to the first active Sentinel configured. + +## Valkey Cluster + +[Cluster support](/valkey/cluster) is based on the same building blocks as non-clustered communication. `io.valkey.springframework.data.connection.ValkeyClusterConnection`, an extension to `ValkeyConnection`, handles the communication with the Valkey Cluster and translates errors into the Spring DAO exception hierarchy. +`ValkeyClusterConnection` instances are created with the `ValkeyConnectionFactory`, which has to be set up with the associated `io.valkey.springframework.data.connection.ValkeyClusterConfiguration`, as shown in the following example: + +*Example 1. Sample ValkeyConnectionFactory Configuration for Valkey Cluster* + +```java +@Component +@ConfigurationProperties(prefix = "spring.valkey.cluster") +public class ClusterConfigurationProperties { + + /* + * spring.valkey.cluster.nodes[0] = 127.0.0.1:7379 + * spring.valkey.cluster.nodes[1] = 127.0.0.1:7380 + * ... + */ + List nodes; + + /** + * Get initial collection of known cluster nodes in format {@code host:port}. + * + * @return + */ + public List getNodes() { + return nodes; + } + + public void setNodes(List nodes) { + this.nodes = nodes; + } +} + +@Configuration +public class AppConfig { + + /** + * Type safe representation of application.properties + */ + @Autowired ClusterConfigurationProperties clusterProperties; + + public @Bean ValkeyConnectionFactory connectionFactory() { + + return new LettuceConnectionFactory( + new ValkeyClusterConfiguration(clusterProperties.getNodes())); + } +} +``` + +:::tip +`ValkeyClusterConfiguration` can also be defined through `ValkeyClusterConfiguration.of(PropertySource)`, which lets you pick up the following properties: + +*Configuration Properties* + +* `spring.valkey.cluster.nodes`: Comma-delimited list of host:port pairs. +* `spring.valkey.cluster.max-redirects`: Number of allowed cluster redirections. +::: + +:::note +The initial configuration points driver libraries to an initial set of cluster nodes. Changes resulting from live cluster reconfiguration are kept only in the native driver and are not written back to the configuration. +::: diff --git a/docs/src/content/docs/redis/drivers.md b/docs/src/content/docs/valkey/drivers.md similarity index 54% rename from docs/src/content/docs/redis/drivers.md rename to docs/src/content/docs/valkey/drivers.md index 92c80ea1c..05cdd8e74 100644 --- a/docs/src/content/docs/redis/drivers.md +++ b/docs/src/content/docs/valkey/drivers.md @@ -3,64 +3,64 @@ title: Drivers description: Drivers documentation --- -One of the first tasks when using Redis and Spring is to connect to the store through the IoC container. +One of the first tasks when using Valkey and Spring is to connect to the store through the IoC container. To do that, a Java connector (or binding) is required. -No matter the library you choose, you need to use only one set of Spring Data Redis APIs (which behaves consistently across all connectors). -The `org.springframework.data.redis.connection` package and its `RedisConnection` and `RedisConnectionFactory` interfaces for working with and retrieving active connections to Redis. +No matter the library you choose, you need to use only one set of Spring Data Valkey APIs (which behaves consistently across all connectors). +The `io.valkey.springframework.data.connection` package and its `ValkeyConnection` and `ValkeyConnectionFactory` interfaces for working with and retrieving active connections to Valkey. -## RedisConnection and RedisConnectionFactory +## ValkeyConnection and ValkeyConnectionFactory -`RedisConnection` provides the core building block for Redis communication, as it handles the communication with the Redis backend. +`ValkeyConnection` provides the core building block for Valkey communication, as it handles the communication with the Valkey backend. It also automatically translates underlying connecting library exceptions to Spring's consistent [DAO exception hierarchy](https://docs.spring.io/spring-framework/reference/data-access.html#dao-exceptions) so that you can switch connectors without any code changes, as the operation semantics remain the same. :::note -For the corner cases where the native library API is required, `RedisConnection` provides a dedicated method (`getNativeConnection`) that returns the raw, underlying object used for communication. +For the corner cases where the native library API is required, `ValkeyConnection` provides a dedicated method (`getNativeConnection`) that returns the raw, underlying object used for communication. ::: -Active `RedisConnection` objects are created through `RedisConnectionFactory`. +Active `ValkeyConnection` objects are created through `ValkeyConnectionFactory`. In addition, the factory acts as `PersistenceExceptionTranslator` objects, meaning that, once declared, they let you do transparent exception translation. For example, you can do exception translation through the use of the `@Repository` annotation and AOP. For more information, see the dedicated [section](https://docs.spring.io/spring-framework/reference/data-access.html#orm-exception-translation) in the Spring Framework documentation. :::note -`RedisConnection` classes are **not** Thread-safe. While the underlying native connection, such as Lettuce's `StatefulRedisConnection`, may be Thread-safe, Spring Data Redis's `LettuceConnection` class itself is not. Therefore, you should **not** share instances of a `RedisConnection` across multiple Threads. This is especially true for transactional, or blocking Redis operations and commands, such as `BLPOP`. In transactional and pipelining operations, for instance, `RedisConnection` holds onto unguarded mutable state to complete the operation correctly, thereby making it unsafe to use with multiple Threads. This is by design. +`ValkeyConnection` classes are **not** Thread-safe. While the underlying native connection, such as Lettuce's `StatefulRedisConnection`, may be Thread-safe, Spring Data Valkey's `LettuceConnection` class itself is not. Therefore, you should **not** share instances of a `ValkeyConnection` across multiple Threads. This is especially true for transactional, or blocking Valkey operations and commands, such as `BLPOP`. In transactional and pipelining operations, for instance, `ValkeyConnection` holds onto unguarded mutable state to complete the operation correctly, thereby making it unsafe to use with multiple Threads. This is by design. ::: :::tip -If you need to share (stateful) Redis resources, like connections, across multiple Threads, for performance reasons or otherwise, then you should acquire the native connection and use the Redis client library (driver) API directly. Alternatively, you can use the `RedisTemplate`, which acquires and manages connections for operations (and Redis commands) in a Thread-safe manner. See [documentation](/redis/template) on `RedisTemplate` for more details. +If you need to share (stateful) Valkey resources, like connections, across multiple Threads, for performance reasons or otherwise, then you should acquire the native connection and use the Valkey client library (driver) API directly. Alternatively, you can use the `ValkeyTemplate`, which acquires and manages connections for operations (and Valkey commands) in a Thread-safe manner. See [documentation](/valkey/template) on `ValkeyTemplate` for more details. ::: :::note Depending on the underlying configuration, the factory can return a new connection or an existing connection (when a pool or shared native connection is used). ::: -The easiest way to work with a `RedisConnectionFactory` is to configure the appropriate connector through the IoC container and inject it into the using class. +The easiest way to work with a `ValkeyConnectionFactory` is to configure the appropriate connector through the IoC container and inject it into the using class. -Unfortunately, currently, not all connectors support all Redis features. +Unfortunately, currently, not all connectors support all Valkey features. When invoking a method on the Connection API that is unsupported by the underlying library, an `UnsupportedOperationException` is thrown. -The following overview explains features that are supported by the individual Redis connectors: +The following overview explains features that are supported by the individual Valkey connectors: -*Table 1. Feature Availability across Redis Connectors* +*Table 1. Feature Availability across Valkey Connectors* | Supported Feature | Lettuce | Jedis | |-------------------|---------|-------| | Standalone Connections | X | X | -| [Master/Replica Connections](/redis/connection-modes#write-to-master-read-from-replica) | X | X | -| [Redis Sentinel](/redis/connection-modes#redis-sentinel) | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | -| [Redis Cluster](/redis/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | +| [Master/Replica Connections](/valkey/connection-modes#write-to-master-read-from-replica) | X | X | +| [Valkey Sentinel](/valkey/connection-modes#valkey-sentinel) | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | +| [Valkey Cluster](/valkey/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | | Transport Channels | TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets | TCP | | Connection Pooling | X (using `commons-pool2`) | X (using `commons-pool2`) | | Other Connection Features | Singleton-connection sharing for non-blocking commands | Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. | | SSL Support | X | X | -| [Pub/Sub](/redis/pubsub) | X | X | -| [Pipelining](/redis/pipelining) | X | X (Pipelining and Transactions mutually exclusive) | -| [Transactions](/redis/transactions) | X | X (Pipelining and Transactions mutually exclusive) | +| [Pub/Sub](/valkey/pubsub) | X | X | +| [Pipelining](/valkey/pipelining) | X | X (Pipelining and Transactions mutually exclusive) | +| [Transactions](/valkey/transactions) | X | X (Pipelining and Transactions mutually exclusive) | | Datatype support | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | | Reactive (non-blocking) API | X | | ## Configuring the Lettuce Connector -[Lettuce](https://github.com/lettuce-io/lettuce-core) is a [Netty](https://netty.io/)-based open-source connector supported by Spring Data Redis through the `org.springframework.data.redis.connection.lettuce` package. +[Lettuce](https://github.com/lettuce-io/lettuce-core) is a [Netty](https://netty.io/)-based open-source connector supported by Spring Data Valkey through the `io.valkey.springframework.data.connection.lettuce` package. *Add the following to the pom.xml files `dependencies` element:* @@ -85,9 +85,9 @@ The following example shows how to create a new Lettuce connection factory: class AppConfig { @Bean - public LettuceConnectionFactory redisConnectionFactory() { + public LettuceConnectionFactory valkeyConnectionFactory() { - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379)); + return new LettuceConnectionFactory(new ValkeyStandaloneConfiguration("server", 6379)); } } ``` @@ -108,24 +108,24 @@ public LettuceConnectionFactory lettuceConnectionFactory() { .shutdownTimeout(Duration.ZERO) .build(); - return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379), clientConfig); + return new LettuceConnectionFactory(new ValkeyStandaloneConfiguration("localhost", 6379), clientConfig); } ``` -For more detailed client configuration tweaks, see `org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration`. +For more detailed client configuration tweaks, see `io.valkey.springframework.data.connection.lettuce.LettuceClientConfiguration`. -Lettuce integrates with Netty's [native transports](https://netty.io/wiki/native-transports.html), letting you use Unix domain sockets to communicate with Redis. +Lettuce integrates with Netty's [native transports](https://netty.io/wiki/native-transports.html), letting you use Unix domain sockets to communicate with Valkey. Make sure to include the appropriate native transport dependencies that match your runtime environment. -The following example shows how to create a Lettuce Connection factory for a Unix domain socket at `/var/run/redis.sock`: +The following example shows how to create a Lettuce Connection factory for a Unix domain socket at `/var/run/valkey.sock`: ```java @Configuration class AppConfig { @Bean - public LettuceConnectionFactory redisConnectionFactory() { + public LettuceConnectionFactory valkeyConnectionFactory() { - return new LettuceConnectionFactory(new RedisSocketConfiguration("/var/run/redis.sock")); + return new LettuceConnectionFactory(new ValkeySocketConfiguration("/var/run/valkey.sock")); } } ``` @@ -136,7 +136,7 @@ Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for ## Configuring the Jedis Connector -[Jedis](https://github.com/redis/jedis) is a community-driven connector supported by the Spring Data Redis module through the `org.springframework.data.redis.connection.jedis` package. +[Jedis](https://github.com/valkey/jedis) is a community-driven connector supported by the Spring Data Valkey module through the `io.valkey.springframework.data.connection.jedis` package. *Add the following to the pom.xml files `dependencies` element:* @@ -146,7 +146,7 @@ Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for - redis.clients + valkey.clients jedis 5.1.2 @@ -161,7 +161,7 @@ In its simplest form, the Jedis configuration looks as follow: class AppConfig { @Bean - public JedisConnectionFactory redisConnectionFactory() { + public JedisConnectionFactory valkeyConnectionFactory() { return new JedisConnectionFactory(); } } @@ -171,12 +171,12 @@ For production use, however, you might want to tweak settings such as the host o ```java @Configuration -class RedisConfiguration { +class ValkeyConfiguration { @Bean - public JedisConnectionFactory redisConnectionFactory() { + public JedisConnectionFactory valkeyConnectionFactory() { - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("server", 6379); + ValkeyStandaloneConfiguration config = new ValkeyStandaloneConfiguration("server", 6379); return new JedisConnectionFactory(config); } } diff --git a/docs/src/content/docs/valkey/getting-started.mdx b/docs/src/content/docs/valkey/getting-started.mdx new file mode 100644 index 000000000..977671a14 --- /dev/null +++ b/docs/src/content/docs/valkey/getting-started.mdx @@ -0,0 +1,86 @@ +--- +title: Getting Started +description: Getting Started documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-valkey) or create a Spring project in [Spring Tools](https://spring.io/tools). +## Examples Repository + +The GitHub [spring-data-examples repository](https://github.com/spring-projects/spring-data-examples) hosts several examples that you can download and play around with to get a feel for how the library works. +## Hello World + +First, you need to set up a running Valkey server. +Spring Data Valkey requires Valkey 2.6 or above and Spring Data Valkey integrates with [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/valkey/jedis), two popular open-source Java libraries for Valkey. + +Now you can create a simple Java application that stores and reads a value to and from Valkey. + +Create the main application to run, as the following example shows: + + + + +```java +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import io.valkey.springframework.data.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory; +import io.valkey.springframework.data.core.ValkeyTemplate; + +public class ValkeyApplication { + + private static final Log LOG = LogFactory.getLog(ValkeyApplication.class); + + public static void main(String[] args) { + + ValkeyConnectionFactory factory = new LettuceConnectionFactory(); + + ValkeyTemplate template = new ValkeyTemplate<>(); + template.setConnectionFactory(factory); + template.afterPropertiesSet(); + + template.opsForValue().set("foo", "bar"); + + LOG.info("Value at foo:" + template.opsForValue().get("foo")); + + template.getConnectionFactory().getConnection().close(); + } +} +``` + + + + +```java +import reactor.core.publisher.Mono; + +import io.valkey.springframework.data.connection.ReactiveValkeyConnectionFactory; +import io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory; +import io.valkey.springframework.data.core.ReactiveValkeyTemplate; +import io.valkey.springframework.data.serialization.ValkeySerializationContext; + +public class ReactiveValkeyApplication { + + public static void main(String[] args) { + + ReactiveValkeyConnectionFactory factory = new LettuceConnectionFactory(); + + ReactiveValkeyTemplate template = new ReactiveValkeyTemplate<>(factory, ValkeySerializationContext.string()); + + template.opsForValue().set("foo", "bar") + .then(template.opsForValue().get("foo")) + .doOnNext(System.out::println) + .then(Mono.fromRunnable(() -> factory.getReactiveConnection().close())) + .subscribe(); + } +} +``` + + + +Even in this simple example, there are a few notable things to point out: + +* You can create an instance of `io.valkey.springframework.data.core.ValkeyTemplate` (or `io.valkey.springframework.data.core.ReactiveValkeyTemplate`for reactive usage) with a `io.valkey.springframework.data.connection.ValkeyConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. +* There's no single way to use Valkey as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/redis/hash-mappers.md b/docs/src/content/docs/valkey/hash-mappers.md similarity index 69% rename from docs/src/content/docs/redis/hash-mappers.md rename to docs/src/content/docs/valkey/hash-mappers.md index d5030c2b7..9cddacde1 100644 --- a/docs/src/content/docs/redis/hash-mappers.md +++ b/docs/src/content/docs/valkey/hash-mappers.md @@ -3,20 +3,20 @@ title: Hash Mappers description: Hash Mappers documentation --- -Data can be stored by using various data structures within Redis. `org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer` can convert objects in [JSON](https://en.wikipedia.org/wiki/JSON) format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): +Data can be stored by using various data structures within Valkey. `io.valkey.springframework.data.serializer.Jackson2JsonValkeySerializer` can convert objects in [JSON](https://en.wikipedia.org/wiki/JSON) format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Valkey hashes. Spring Data Valkey offers various strategies for mapping data to hashes (depending on the use case): -* Direct mapping, by using `org.springframework.data.redis.core.HashOperations` and a [serializer](/redis/template#serializers) -* Using [Redis Repositories](/repositories) -* Using `org.springframework.data.redis.hash.HashMapper` and `org.springframework.data.redis.core.HashOperations` +* Direct mapping, by using `io.valkey.springframework.data.core.HashOperations` and a [serializer](/valkey/template#serializers) +* Using [Valkey Repositories](/repositories) +* Using `io.valkey.springframework.data.hash.HashMapper` and `io.valkey.springframework.data.core.HashOperations` ## Hash Mappers -Hash mappers are converters of map objects to a `Map` and back. `org.springframework.data.redis.hash.HashMapper` is intended for using with Redis Hashes. +Hash mappers are converters of map objects to a `Map` and back. `io.valkey.springframework.data.hash.HashMapper` is intended for using with Valkey Hashes. Multiple implementations are available: -* `org.springframework.data.redis.hash.BeanUtilsHashMapper` using Spring's [BeanUtils](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html). -* `org.springframework.data.redis.hash.ObjectHashMapper` using [Object-to-Hash Mapping](/redis/redis-repositories/mapping). +* `io.valkey.springframework.data.hash.BeanUtilsHashMapper` using Spring's [BeanUtils](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html). +* `io.valkey.springframework.data.hash.ObjectHashMapper` using [Object-to-Hash Mapping](/valkey/valkey-repositories/mapping). * [`Jackson2HashMapper`](#jackson2hashmapper) using [FasterXML Jackson](https://github.com/FasterXML/jackson). The following example shows one way to implement hash mapping: @@ -31,7 +31,7 @@ public class Person { public class HashMapping { - @Resource(name = "redisTemplate") + @Resource(name = "valkeyTemplate") HashOperations hashOperations; HashMapper mapper = new ObjectHashMapper(); @@ -52,7 +52,7 @@ public class HashMapping { ## Jackson2HashMapper -`org.springframework.data.redis.hash.Jackson2HashMapper` provides Redis Hash mapping for domain objects by using [FasterXML Jackson](https://github.com/FasterXML/jackson). +`io.valkey.springframework.data.hash.Jackson2HashMapper` provides Valkey Hash mapping for domain objects by using [FasterXML Jackson](https://github.com/FasterXML/jackson). `Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. diff --git a/docs/src/content/docs/valkey/pipelining.md b/docs/src/content/docs/valkey/pipelining.md new file mode 100644 index 000000000..a5a7e842b --- /dev/null +++ b/docs/src/content/docs/valkey/pipelining.md @@ -0,0 +1,47 @@ +--- +title: Pipelining +description: Pipelining documentation +--- + +Valkey provides support for [pipelining](https://valkey.io/topics/pipelining), which involves sending multiple commands to the server without waiting for the replies and then reading the replies in a single step. Pipelining can improve performance when you need to send several commands in a row, such as adding many elements to the same List. + +Spring Data Valkey provides several `ValkeyTemplate` methods for running commands in a pipeline. If you do not care about the results of the pipelined operations, you can use the standard `execute` method, passing `true` for the `pipeline` argument. The `executePipelined` methods run the provided `ValkeyCallback` or `SessionCallback` in a pipeline and return the results, as shown in the following example: + +```java +//pop a specified number of items from a queue +List results = stringValkeyTemplate.executePipelined( + new ValkeyCallback() { + public Object doInValkey(ValkeyConnection connection) throws DataAccessException { + StringValkeyConnection stringValkeyConn = (StringValkeyConnection)connection; + for(int i=0; i< batchSize; i++) { + stringValkeyConn.rPop("myqueue"); + } + return null; + } +}); +``` + +The preceding example runs a bulk right pop of items from a queue in a pipeline. +The `results` `List` contains all the popped items. `ValkeyTemplate` uses its value, hash key, and hash value serializers to deserialize all results before returning, so the returned items in the preceding example are Strings. +There are additional `executePipelined` methods that let you pass a custom serializer for pipelined results. + +Note that the value returned from the `ValkeyCallback` is required to be `null`, as this value is discarded in favor of returning the results of the pipelined commands. + +:::tip +The Lettuce driver supports fine-grained flush control that allows to either flush commands as they appear, buffer or send them at connection close. +::: + +```java +LettuceConnectionFactory factory = // ... +factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3)); // (1) +``` +```text +1. Buffer locally and flush after every 3rd command. +``` + +:::note +Pipelining is limited to Valkey Standalone. +::: + +Valkey Cluster is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Same-slot keys are fully supported. diff --git a/docs/src/content/docs/redis/pubsub.mdx b/docs/src/content/docs/valkey/pubsub.mdx similarity index 51% rename from docs/src/content/docs/redis/pubsub.mdx rename to docs/src/content/docs/valkey/pubsub.mdx index 2b64f166b..708acaf90 100644 --- a/docs/src/content/docs/redis/pubsub.mdx +++ b/docs/src/content/docs/valkey/pubsub.mdx @@ -5,33 +5,33 @@ description: Pubsub documentation import { Tabs, TabItem } from '@astrojs/starlight/components'; -Spring Data provides dedicated messaging integration for Redis, similar in functionality and naming to the JMS integration in Spring Framework. +Spring Data provides dedicated messaging integration for Valkey, similar in functionality and naming to the JMS integration in Spring Framework. -Redis messaging can be roughly divided into two areas of functionality: +Valkey messaging can be roughly divided into two areas of functionality: * Publication or production of messages * Subscription or consumption of messages -This is an example of the pattern often called Publish/Subscribe (Pub/Sub for short). The `RedisTemplate` class is used for message production. For asynchronous reception similar to Java EE's message-driven bean style, Spring Data provides a dedicated message listener container that is used to create Message-Driven POJOs (MDPs) and, for synchronous reception, the `RedisConnection` contract. +This is an example of the pattern often called Publish/Subscribe (Pub/Sub for short). The `ValkeyTemplate` class is used for message production. For asynchronous reception similar to Java EE's message-driven bean style, Spring Data provides a dedicated message listener container that is used to create Message-Driven POJOs (MDPs) and, for synchronous reception, the `ValkeyConnection` contract. -The `org.springframework.data.redis.connection` and `org.springframework.data.redis.listener` packages provide the core functionality for Redis messaging. +The `io.valkey.springframework.data.connection` and `io.valkey.springframework.data.listener` packages provide the core functionality for Valkey messaging. ## Publishing (Sending Messages) -To publish a message, you can use, as with the other operations, either the low-level `[Reactive]RedisConnection` or the high-level `[Reactive]RedisOperations`. Both entities offer the `publish` method, which accepts the message and the destination channel as arguments. While `RedisConnection` requires raw data (array of bytes), the `[Reactive]RedisOperations` lets arbitrary objects be passed in as messages, as shown in the following example: +To publish a message, you can use, as with the other operations, either the low-level `[Reactive]ValkeyConnection` or the high-level `[Reactive]ValkeyOperations`. Both entities offer the `publish` method, which accepts the message and the destination channel as arguments. While `ValkeyConnection` requires raw data (array of bytes), the `[Reactive]ValkeyOperations` lets arbitrary objects be passed in as messages, as shown in the following example: ```java // send message through connection -RedisConnection con = … +ValkeyConnection con = … byte[] msg = … byte[] channel = … con.pubSubCommands().publish(msg, channel); -// send message through RedisOperations -RedisOperations operations = … +// send message through ValkeyOperations +ValkeyOperations operations = … Long numberOfClients = operations.convertAndSend("hello!", "world"); ``` @@ -40,13 +40,13 @@ Long numberOfClients = operations.convertAndSend("hello!", "world"); ```java // send message through connection -ReactiveRedisConnection con = … +ReactiveValkeyConnection con = … ByteBuffer[] msg = … ByteBuffer[] channel = … con.pubSubCommands().publish(msg, channel); -// send message through ReactiveRedisOperations -ReactiveRedisOperations operations = … +// send message through ReactiveValkeyOperations +ReactiveValkeyOperations operations = … Mono numberOfClients = operations.convertAndSend("hello!", "world"); ``` @@ -57,10 +57,10 @@ Mono numberOfClients = operations.convertAndSend("hello!", "world"); On the receiving side, one can subscribe to one or multiple channels either by naming them directly or by using pattern matching. The latter approach is quite useful, as it not only lets multiple subscriptions be created with one command but can also listen on channels not yet created at subscription time (as long as they match the pattern). -At the low-level, `RedisConnection` offers the `subscribe` and `pSubscribe` methods that map the Redis commands for subscribing by channel or by pattern, respectively. Note that multiple channels or patterns can be used as arguments. To change the subscription of a connection or query whether it is listening, `RedisConnection` provides the `getSubscription` and `isSubscribed` methods. +At the low-level, `ValkeyConnection` offers the `subscribe` and `pSubscribe` methods that map the Valkey commands for subscribing by channel or by pattern, respectively. Note that multiple channels or patterns can be used as arguments. To change the subscription of a connection or query whether it is listening, `ValkeyConnection` provides the `getSubscription` and `isSubscribed` methods. :::note -Subscription commands in Spring Data Redis are blocking. That is, calling subscribe on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the subscription is canceled, which happens when another thread invokes `unsubscribe` or `pUnsubscribe` on the *same* connection. See "[Message Listener Containers](#message-listener-containers)" (later in this document) for a solution to this problem. +Subscription commands in Spring Data Valkey are blocking. That is, calling subscribe on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the subscription is canceled, which happens when another thread invokes `unsubscribe` or `pUnsubscribe` on the *same* connection. See "[Message Listener Containers](#message-listener-containers)" (later in this document) for a solution to this problem. ::: As mentioned earlier, once subscribed, a connection starts waiting for messages. Only commands that add new subscriptions, modify existing subscriptions, and cancel existing subscriptions are allowed. Invoking anything other than `subscribe`, `pSubscribe`, `unsubscribe`, or `pUnsubscribe` throws an exception. @@ -69,19 +69,19 @@ In order to subscribe to messages, one needs to implement the `MessageListener` ### Message Listener Containers -Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `org.springframework.data.redis.listener.RedisMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). +Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). -`org.springframework.data.redis.listener.RedisMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Redis channel and drive the `org.springframework.data.redis.connection.MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. +`io.valkey.springframework.data.listener.ValkeyMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Valkey channel and drive the `io.valkey.springframework.data.connection.MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Valkey infrastructure concerns to the framework. -A `org.springframework.data.redis.connection.MessageListener` can additionally implement `org.springframework.data.redis.connection.SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. +A `io.valkey.springframework.data.connection.MessageListener` can additionally implement `io.valkey.springframework.data.connection.SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. -Furthermore, to minimize the application footprint, `org.springframework.data.redis.listener.RedisMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. +Furthermore, to minimize the application footprint, `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `ValkeyConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. To help with the asynchronous nature of messages, the container requires a `java.util.concurrent.Executor` (or Spring's `TaskExecutor`) for dispatching the messages. Depending on the load, the number of listeners, or the runtime environment, you should change or tweak the executor to better serve your needs. In particular, in managed environments (such as app servers), it is highly recommended to pick a proper `TaskExecutor` to take advantage of its runtime. ### The MessageListenerAdapter -The `org.springframework.data.redis.listener.adapter.MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). +The `io.valkey.springframework.data.listener.adapter.MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). Consider the following interface definition: @@ -96,7 +96,7 @@ public interface MessageDelegate { } ``` -Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `org.springframework.data.redis.listener.adapter.MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: +Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `io.valkey.springframework.data.listener.adapter.MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: ```java public class DefaultMessageDelegate implements MessageDelegate { @@ -104,7 +104,7 @@ public class DefaultMessageDelegate implements MessageDelegate { } ``` -Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Redis dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: +Notice how the above implementation of the `MessageDelegate` interface (the above `DefaultMessageDelegate` class) has *no* Valkey dependencies at all. It truly is a POJO that we make into an MDP with the following configuration: @@ -126,9 +126,9 @@ class MyConfig { } @Bean - RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) { + ValkeyMessageListenerContainer valkeyMessageListenerContainer(ValkeyConnectionFactory connectionFactory, MessageListenerAdapter listener) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + ValkeyMessageListenerContainer container = new ValkeyMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(listener, ChannelTopic.of("chatroom")); return container; @@ -143,17 +143,17 @@ class MyConfig { + http://www.springframework.org/schema/valkey https://www.springframework.org/schema/valkey/spring-valkey.xsd"> - + - - + + - + ... ``` @@ -165,21 +165,21 @@ class MyConfig { The listener topic can be either a channel (for example, `topic="chatroom"` respective `Topic.channel("chatroom")`) or a pattern (for example, `topic="*room"` respective `Topic.pattern("*room")`). ::: -The preceding example uses the Redis namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: +The preceding example uses the Valkey namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: ```xml - + - + - + - + @@ -188,17 +188,17 @@ The preceding example uses the Redis namespace to declare the message listener c ``` -Each time a message is received, the adapter automatically and transparently performs translation (using the configured `RedisSerializer`) between the low-level format and the required object type. Any exception caused by the method invocation is caught and handled by the container (by default, exceptions get logged). +Each time a message is received, the adapter automatically and transparently performs translation (using the configured `ValkeySerializer`) between the low-level format and the required object type. Any exception caused by the method invocation is caught and handled by the container (by default, exceptions get logged). ## Reactive Message Listener Container -Spring Data offers `org.springframework.data.redis.listener.ReactiveRedisMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. +Spring Data offers `io.valkey.springframework.data.listener.ReactiveValkeyMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. The message listener container itself does not require external threading resources. It uses the driver threads to publish messages. ```java -ReactiveRedisConnectionFactory factory = … -ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); +ReactiveValkeyConnectionFactory factory = … +ReactiveValkeyMessageListenerContainer container = new ReactiveValkeyMessageListenerContainer(factory); Flux> stream = container.receive(ChannelTopic.of("my-channel")); ``` @@ -207,25 +207,25 @@ To await and ensure proper subscription, you can use the `receiveLater` method t The resulting `Mono` completes with an inner publisher as a result of completing the subscription to the given topics. By intercepting `onNext` signals, you can synchronize server-side subscriptions. ```java -ReactiveRedisConnectionFactory factory = … -ReactiveRedisMessageListenerContainer container = new ReactiveRedisMessageListenerContainer(factory); +ReactiveValkeyConnectionFactory factory = … +ReactiveValkeyMessageListenerContainer container = new ReactiveValkeyMessageListenerContainer(factory); Mono>> stream = container.receiveLater(ChannelTopic.of("my-channel")); -stream.doOnNext(inner -> // notification hook when Redis subscriptions are synchronized with the server) +stream.doOnNext(inner -> // notification hook when Valkey subscriptions are synchronized with the server) .flatMapMany(Function.identity()) .…; ``` ### Subscribing via template API -As mentioned above you can directly use `org.springframework.data.redis.core.ReactiveRedisTemplate` to subscribe to channels / patterns. This approach +As mentioned above you can directly use `io.valkey.springframework.data.core.ReactiveValkeyTemplate` to subscribe to channels / patterns. This approach offers a straight forward, though limited solution as you lose the option to add subscriptions after the initial ones. Nevertheless you still can control the message stream via the returned `Flux` using eg. `take(Duration)`. When done reading, on error or cancellation all bound resources are freed again. ```java -redisTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> { +valkeyTemplate.listenToChannel("channel1", "channel2").doOnNext(msg -> { // message processing ... }).subscribe(); ``` diff --git a/docs/src/content/docs/valkey/scripting.mdx b/docs/src/content/docs/valkey/scripting.mdx new file mode 100644 index 000000000..48d31f78f --- /dev/null +++ b/docs/src/content/docs/valkey/scripting.mdx @@ -0,0 +1,82 @@ +--- +title: Scripting +description: Scripting documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Valkey versions 2.6 and higher provide support for running Lua scripts through the [eval](https://valkey.io/commands/eval) and [evalsha](https://valkey.io/commands/evalsha) commands. Spring Data Valkey provides a high-level abstraction for running scripts that handles serialization and automatically uses the Valkey script cache. + +Scripts can be run by calling the `execute` methods of `ValkeyTemplate` and `ReactiveValkeyTemplate`. Both use a configurable `io.valkey.springframework.data.core.script.ScriptExecutor` (or `io.valkey.springframework.data.core.script.ReactiveScriptExecutor`) to run the provided script. By default, the `io.valkey.springframework.data.core.script.ScriptExecutor` (or `io.valkey.springframework.data.core.script.ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. + +The default `io.valkey.springframework.data.core.script.ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Valkey script cache. + +The following example runs a common "check-and-set" scenario by using a Lua script. This is an ideal use case for a Valkey script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. + +```java +@Bean +public ValkeyScript script() { + + ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua")); + return ValkeyScript.of(scriptSource, Boolean.class); +} +``` + + + + +```java +public class Example { + + @Autowired + ValkeyOperations valkeyOperations; + + @Autowired + ValkeyScript script; + + public boolean checkAndSet(String expectedValue, String newValue) { + return valkeyOperations.execute(script, List.of("key"), expectedValue, newValue); + } +} +``` + + + + +```java +public class Example { + + @Autowired + ReactiveValkeyOperations valkeyOperations; + + @Autowired + ValkeyScript script; + + public Flux checkAndSet(String expectedValue, String newValue) { + return valkeyOperations.execute(script, List.of("key"), expectedValue, newValue); + } +} +``` + + + + +```lua +-- checkandset.lua +local current = valkey.call('GET', KEYS[1]) +if current == ARGV[1] + then valkey.call('SET', KEYS[1], ARGV[2]) + return true +end +return false +``` + +The preceding code configures a `io.valkey.springframework.data.core.script.ValkeyScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). + +:::tip +It is ideal to configure a single instance of `DefaultValkeyScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. +::: + +The `checkAndSet` method above then runs the scripts. Scripts can be run within a `io.valkey.springframework.data.core.SessionCallback` as part of a transaction or pipeline. See "[Valkey Transactions](/valkey/transactions)" and "[Pipelining](/valkey/pipelining)" for more information. + +The scripting support provided by Spring Data Valkey also lets you schedule Valkey scripts for periodic running by using the Spring Task and Scheduler abstractions. See the [Spring Framework](https://spring.io/projects/spring-framework/) documentation for more details. diff --git a/docs/src/content/docs/valkey/support-classes.mdx b/docs/src/content/docs/valkey/support-classes.mdx new file mode 100644 index 000000000..5e3471d64 --- /dev/null +++ b/docs/src/content/docs/valkey/support-classes.mdx @@ -0,0 +1,72 @@ +--- +title: Support Classes +description: Support Classes documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Package `io.valkey.springframework.data.support` offers various reusable components that rely on Valkey as a backing store. +Currently, the package contains various JDK-based interface implementations on top of Valkey, such as [atomic](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html) counters and JDK [Collections](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html). + +:::note +`io.valkey.springframework.data.support.collections.ValkeyList` is forward-compatible with Java 21 `SequencedCollection`. +::: + +The atomic counters make it easy to wrap Valkey key incrementation while the collections allow easy management of Valkey keys with minimal storage exposure or API leakage. +In particular, the `io.valkey.springframework.data.support.collections.ValkeySet` and `io.valkey.springframework.data.support.collections.ValkeyZSet` interfaces offer easy access to the set operations supported by Valkey, such as `intersection` and `union`. `io.valkey.springframework.data.support.collections.ValkeyList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Valkey, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. +The following example shows the configuration for a bean that uses a `io.valkey.springframework.data.support.collections.ValkeyList`: + + + + +```java +@Configuration +class MyConfig { + + // … + + @Bean + ValkeyList stringValkeyTemplate(ValkeyTemplate valkeyTemplate) { + return new DefaultValkeyList<>(template, "queue-key"); + } +} +``` + + + + +```xml + + + + + + + + + +``` + + + + +The following example shows a Java configuration example for a `Deque`: + +```java +public class AnotherExample { + + // injected + private Deque queue; + + public void addTag(String tag) { + queue.push(tag); + } +} +``` + +As shown in the preceding example, the consuming code is decoupled from the actual storage implementation. +In fact, there is no indication that Valkey is used underneath. +This makes moving from development to production environments transparent and highly increases testability (the Valkey implementation can be replaced with an in-memory one). diff --git a/docs/src/content/docs/valkey/template.mdx b/docs/src/content/docs/valkey/template.mdx new file mode 100644 index 000000000..ac0f9ce3e --- /dev/null +++ b/docs/src/content/docs/valkey/template.mdx @@ -0,0 +1,341 @@ +--- +title: Working with Objects through ValkeyTemplate +description: Template documentation +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Most users are likely to use `io.valkey.springframework.data.core.ValkeyTemplate` and its corresponding package, `io.valkey.springframework.data.core` or its reactive variant `io.valkey.springframework.data.core.ReactiveValkeyTemplate`. +The template is, in fact, the central class of the Valkey module, due to its rich feature set. +The template offers a high-level abstraction for Valkey interactions. +While `[Reactive]ValkeyConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. + +The `io.valkey.springframework.data.core.ValkeyTemplate` class implements the `io.valkey.springframework.data.core.ValkeyOperations` interface and its reactive variant `io.valkey.springframework.data.core.ReactiveValkeyTemplate` implements `io.valkey.springframework.data.core.ReactiveValkeyOperations`. + +:::note +The preferred way to reference operations on a `[Reactive]ValkeyTemplate` instance is through the +`[Reactive]ValkeyOperations` interface. +::: + +Moreover, the template provides operations views (following the grouping from the Valkey command [reference](https://valkey.io/commands)) that offer rich, generified interfaces for working against a certain type or certain key (through the `KeyBound` interfaces) as described in the following table: + +
+Operational views + + + + +| Interface | Description | +|-----------|-------------| +| *Key Type Operations* | | +| `io.valkey.springframework.data.core.GeoOperations` | Valkey geospatial operations, such as `GEOADD`, `GEORADIUS`,... | +| `io.valkey.springframework.data.core.HashOperations` | Valkey hash operations | +| `io.valkey.springframework.data.core.HyperLogLogOperations` | Valkey HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... | +| `io.valkey.springframework.data.core.ListOperations` | Valkey list operations | +| `io.valkey.springframework.data.core.SetOperations` | Valkey set operations | +| `io.valkey.springframework.data.core.ValueOperations` | Valkey string (or value) operations | +| `io.valkey.springframework.data.core.ZSetOperations` | Valkey zset (or sorted set) operations | +| *Key Bound Operations* | | +| `io.valkey.springframework.data.core.BoundGeoOperations` | Valkey key bound geospatial operations | +| `io.valkey.springframework.data.core.BoundHashOperations` | Valkey hash key bound operations | +| `io.valkey.springframework.data.core.BoundKeyOperations` | Valkey key bound operations | +| `io.valkey.springframework.data.core.BoundListOperations` | Valkey list key bound operations | +| `io.valkey.springframework.data.core.BoundSetOperations` | Valkey set key bound operations | +| `io.valkey.springframework.data.core.BoundValueOperations` | Valkey string (or value) key bound operations | +| `io.valkey.springframework.data.core.BoundZSetOperations` | Valkey zset (or sorted set) key bound operations | + + + + +| Interface | Description | +|-----------|-------------| +| *Key Type Operations* | | +| `io.valkey.springframework.data.core.ReactiveGeoOperations` | Valkey geospatial operations such as `GEOADD`, `GEORADIUS`, and others | +| `io.valkey.springframework.data.core.ReactiveHashOperations` | Valkey hash operations | +| `io.valkey.springframework.data.core.ReactiveHyperLogLogOperations` | Valkey HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) | +| `io.valkey.springframework.data.core.ReactiveListOperations` | Valkey list operations | +| `io.valkey.springframework.data.core.ReactiveSetOperations` | Valkey set operations | +| `io.valkey.springframework.data.core.ReactiveValueOperations` | Valkey string (or value) operations | +| `io.valkey.springframework.data.core.ReactiveZSetOperations` | Valkey zset (or sorted set) operations | + + + + +
+ +Once configured, the template is thread-safe and can be reused across multiple instances. + +`ValkeyTemplate` uses a Java-based serializer for most of its operations. +This means that any object written or read by the template is serialized and deserialized through Java. + +You can change the serialization mechanism on the template, and the Valkey module offers several implementations, which are available in the `io.valkey.springframework.data.serializer` package. +See [Serializers](#serializers) for more information. +You can also set any of the serializers to null and use ValkeyTemplate with raw byte arrays by setting the `enableDefaultSerializer` property to `false`. +Note that the template requires all keys to be non-null. +However, values can be null as long as the underlying serializer accepts them. +Read the Javadoc of each serializer for more information. + +For cases where you need a certain template view, declare the view as a dependency and inject the template. +The container automatically performs the conversion, eliminating the `opsFor[X]` calls, as shown in the following example: + +*Configuring Template API* + + + + +```java +@Configuration +class MyConfig { + + @Bean + LettuceConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + ValkeyTemplate valkeyTemplate(ValkeyConnectionFactory connectionFactory) { + + ValkeyTemplate template = new ValkeyTemplate<>(); + template.setConnectionFactory(connectionFactory); + return template; + } +} +``` + + + + +```java +@Configuration +class MyConfig { + + @Bean + LettuceConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + ReactiveValkeyTemplate ReactiveValkeyTemplate(ReactiveValkeyConnectionFactory connectionFactory) { + return new ReactiveValkeyTemplate<>(connectionFactory, ValkeySerializationContext.string()); + } +} +``` + + + + +```xml + + + + + + + ... + + +``` + + + + +*Pushing an item to a List using `[Reactive]ValkeyTemplate`* + + + + +```java +public class Example { + + // inject the actual operations + @Autowired + private ValkeyOperations operations; + + // inject the template as ListOperations + @Resource(name="valkeyTemplate") + private ListOperations listOps; + + public void addLink(String userId, URL url) { + listOps.leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +```java +public class Example { + + // inject the actual template + @Autowired + private ReactiveValkeyOperations operations; + + public Mono addLink(String userId, URL url) { + return operations.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +## String-focused Convenience Classes + +Since it is quite common for the keys and values stored in Valkey to be `java.lang.String`, the Valkey modules provides two extensions to `ValkeyConnection` and `ValkeyTemplate`, respectively the `StringValkeyConnection` (and its `DefaultStringValkeyConnection` implementation) and `StringValkeyTemplate` as a convenient one-stop solution for intensive String operations. +In addition to being bound to `String` keys, the template and the connection use the `StringValkeySerializer` underneath, which means the stored keys and values are human-readable (assuming the same encoding is used both in Valkey and your code). +The following listings show an example: + + + + +```java +@Configuration +class ValkeyConfiguration { + + @Bean + LettuceConnectionFactory valkeyConnectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + StringValkeyTemplate stringValkeyTemplate(ValkeyConnectionFactory valkeyConnectionFactory) { + + StringValkeyTemplate template = new StringValkeyTemplate(); + template.setConnectionFactory(valkeyConnectionFactory); + return template; + } +} +``` + + + + +```java +@Configuration +class ValkeyConfiguration { + + @Bean + LettuceConnectionFactory valkeyConnectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + ReactiveStringValkeyTemplate reactiveValkeyTemplate(ReactiveValkeyConnectionFactory factory) { + return new ReactiveStringValkeyTemplate<>(factory); + } +} +``` + + + + +```xml + + + + + + + + +``` + + + + + + + +```java +public class Example { + + @Autowired + private StringValkeyTemplate valkeyTemplate; + + public void addLink(String userId, URL url) { + valkeyTemplate.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +```java +public class Example { + + @Autowired + private ReactiveStringValkeyTemplate valkeyTemplate; + + public Mono addLink(String userId, URL url) { + return valkeyTemplate.opsForList().leftPush(userId, url.toExternalForm()); + } +} +``` + + + + +As with the other Spring templates, `ValkeyTemplate` and `StringValkeyTemplate` let you talk directly to Valkey through the `ValkeyCallback` interface. +This feature gives complete control to you, as it talks directly to the `ValkeyConnection`. +Note that the callback receives an instance of `StringValkeyConnection` when a `StringValkeyTemplate` is used. +The following example shows how to use the `ValkeyCallback` interface: + +```java +public void useCallback() { + + valkeyOperations.execute(new ValkeyCallback() { + public Object doInValkey(ValkeyConnection connection) throws DataAccessException { + Long size = connection.dbSize(); + // Can cast to StringValkeyConnection if using a StringValkeyTemplate + ((StringValkeyConnection)connection).set("key", "value"); + } + }); +} +``` + +## Serializers + +From the framework perspective, the data stored in Valkey is only bytes. +While Valkey itself supports various types, for the most part, these refer to the way the data is stored rather than what it represents. +It is up to the user to decide whether the information gets translated into strings or any other objects. + +In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Valkey in the `io.valkey.springframework.data.serializer` package. + +This package contains two types of serializers that, as the name implies, take care of the serialization process: + +* Two-way serializers based on `io.valkey.springframework.data.serializer.ValkeySerializer`. +* Element readers and writers that use `ValkeyElementReader` and ``ValkeyElementWriter``. + +The main difference between these variants is that `ValkeySerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. + +Multiple implementations are available (including two that have been already mentioned in this documentation): + +* `io.valkey.springframework.data.serializer.JdkSerializationValkeySerializer`, which is used by default for `io.valkey.springframework.data.cache.ValkeyCache` and `io.valkey.springframework.data.core.ValkeyTemplate`. +* the `StringValkeySerializer`. + +However, one can use `OxmSerializer` for Object/XML mapping through Spring [OXM](https://docs.spring.io/spring-framework/reference/data-access.html#oxm) support or `io.valkey.springframework.data.serializer.Jackson2JsonValkeySerializer` or `io.valkey.springframework.data.serializer.GenericJackson2JsonValkeySerializer` for storing data in [JSON](https://en.wikipedia.org/wiki/JSON) format. + +Do note that the storage format is not limited only to values. +It can be used for keys, values, or hashes without any restrictions. + +:::danger +By default, `io.valkey.springframework.data.cache.ValkeyCache` and `io.valkey.springframework.data.core.ValkeyTemplate` are configured to use Java native serialization. +Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. +Manipulated input could lead to unwanted code being run in the application during the deserialization step. +As a consequence, do not use serialization in untrusted environments. +In general, we strongly recommend any other message format (such as JSON) instead. + +If you are concerned about security vulnerabilities due to Java serialization, consider the general-purpose serialization filter mechanism at the core JVM level: + +* [Filter Incoming Serialization Data](https://docs.oracle.com/en/java/javase/17/core/serialization-filtering1.html). +* [JEP 290](https://openjdk.org/jeps/290). +* [OWASP: Deserialization of untrusted data](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data). +::: diff --git a/docs/src/content/docs/valkey/transactions.md b/docs/src/content/docs/valkey/transactions.md new file mode 100644 index 000000000..4b3d0e51f --- /dev/null +++ b/docs/src/content/docs/valkey/transactions.md @@ -0,0 +1,117 @@ +--- +title: Valkey Transactions +description: Transactions documentation +--- + +Valkey provides support for [transactions](https://valkey.io/topics/transactions) through the `multi`, `exec`, and `discard` commands. +These operations are available on `io.valkey.springframework.data.core.ValkeyTemplate`. +However, `ValkeyTemplate` is not guaranteed to run all the operations in the transaction with the same connection. + +Spring Data Valkey provides the `io.valkey.springframework.data.core.SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Valkey transactions.The following example uses the `multi` method: + +```java +//execute a transaction +List txResults = valkeyOperations.execute(new SessionCallback>() { + public List execute(ValkeyOperations operations) throws DataAccessException { + operations.multi(); + operations.opsForSet().add("key", "value1"); + + // This will contain the results of all operations in the transaction + return operations.exec(); + } +}); +System.out.println("Number of items added to set: " + txResults.get(0)); +``` + +`ValkeyTemplate` uses its value, hash key, and hash value serializers to deserialize all results of `exec` before returning. +There is an additional `exec` method that lets you pass a custom serializer for transaction results. + +It is worth mentioning that in case between `multi()` and `exec()` an exception happens (e.g. a timeout exception in case Valkey does not respond within the timeout) then the connection may get stuck in a transactional state. +To prevent such a situation need have to discard the transactional state to clear the connection: + +```java +List txResults = valkeyOperations.execute(new SessionCallback>() { + public List execute(ValkeyOperations operations) throws DataAccessException { + boolean transactionStateIsActive = true; + try { + operations.multi(); + operations.opsForSet().add("key", "value1"); + + // This will contain the results of all operations in the transaction + return operations.exec(); + } catch (RuntimeException e) { + operations.discard(); + throw e; + } + } +}); +``` + +## `@Transactional` Support + +By default, `ValkeyTemplate` does not participate in managed Spring transactions. +If you want `ValkeyTemplate` to make use of Valkey transaction when using `@Transactional` or `TransactionTemplate`, you need to be explicitly enable transaction support for each `ValkeyTemplate` by setting `setEnableTransactionSupport(true)`. +Enabling transaction support binds `ValkeyConnection` to the current transaction backed by a `ThreadLocal`. +If the transaction finishes without errors, the Valkey transaction gets commited with `EXEC`, otherwise rolled back with `DISCARD`. +Valkey transactions are batch-oriented. +Commands issued during an ongoing transaction are queued and only applied when committing the transaction. + +Spring Data Valkey distinguishes between read-only and write commands in an ongoing transaction. +Read-only commands, such as `KEYS`, are piped to a fresh (non-thread-bound) `ValkeyConnection` to allow reads. +Write commands are queued by `ValkeyTemplate` and applied upon commit. + +The following example shows how to configure transaction management: + +*Example 1. Configuration enabling Transaction Management* + +```java +@Configuration +@EnableTransactionManagement // (1) +public class ValkeyTxContextConfiguration { + + @Bean + public StringValkeyTemplate valkeyTemplate() { + StringValkeyTemplate template = new StringValkeyTemplate(valkeyConnectionFactory()); + // explicitly enable transaction support + template.setEnableTransactionSupport(true); // (2) + return template; + } + + @Bean + public ValkeyConnectionFactory valkeyConnectionFactory() { + // jedis || Lettuce + } + + @Bean + public PlatformTransactionManager transactionManager() throws SQLException { + return new DataSourceTransactionManager(dataSource()); // (3) + } + + @Bean + public DataSource dataSource() throws SQLException { + // ... + } +} +``` +```text +1. Configures a Spring Context to enable [declarative transaction management](https://docs.spring.io/spring-framework/reference/data-access.html#transaction-declarative). +2. Configures `ValkeyTemplate` to participate in transactions by binding connections to the current thread. +3. Transaction management requires a `PlatformTransactionManager`. +Spring Data Valkey does not ship with a `PlatformTransactionManager` implementation. +Assuming your application uses JDBC, Spring Data Valkey can participate in transactions by using existing transaction managers. +``` + +The following examples each demonstrate a usage constraint: + +*Example 2. Usage Constraints* + +```java +// must be performed on thread-bound connection +template.opsForValue().set("thing1", "thing2"); + +// read operation must be run on a free (not transaction-aware) connection +template.keys("*"); + +// returns null as values set within a transaction are not visible +template.opsForValue().get("thing1"); +``` diff --git a/docs/src/content/docs/valkey/valkey-cache.md b/docs/src/content/docs/valkey/valkey-cache.md new file mode 100644 index 000000000..cd08281ca --- /dev/null +++ b/docs/src/content/docs/valkey/valkey-cache.md @@ -0,0 +1,269 @@ +--- +title: Valkey Cache +description: Valkey Cache documentation +--- + +Spring Data Valkey provides an implementation of Spring Framework's [Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration.html#cache) in the `io.valkey.springframework.data.cache` package. +To use Valkey as a backing implementation, add `io.valkey.springframework.data.cache.ValkeyCacheManager` to your configuration, as follows: + +```java +@Bean +public ValkeyCacheManager cacheManager(ValkeyConnectionFactory connectionFactory) { + return ValkeyCacheManager.create(connectionFactory); +} +``` + +`ValkeyCacheManager` behavior can be configured with `io.valkey.springframework.data.cache.ValkeyCacheManager$ValkeyCacheManagerBuilder`, letting you set the default `io.valkey.springframework.data.cache.ValkeyCacheManager`, transaction behavior, and predefined caches. + +```java +ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(connectionFactory) + .cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig()) + .transactionAware() + .withInitialCacheConfigurations(Collections.singletonMap("predefined", + ValkeyCacheConfiguration.defaultCacheConfig().disableCachingNullValues())) + .build(); +``` + +As shown in the preceding example, `ValkeyCacheManager` allows custom configuration on a per-cache basis. + +The behavior of `io.valkey.springframework.data.cache.ValkeyCache` created by `io.valkey.springframework.data.cache.ValkeyCacheManager` is defined with `ValkeyCacheConfiguration`. +The configuration lets you set key expiration times, prefixes, and `ValkeySerializer` implementations for converting to and from the binary storage format, as shown in the following example: + +```java +ValkeyCacheConfiguration cacheConfiguration = ValkeyCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(1)) + .disableCachingNullValues(); +``` + +`io.valkey.springframework.data.cache.ValkeyCacheManager` defaults to a lock-free `io.valkey.springframework.data.cache.ValkeyCacheWriter` for reading and writing binary values. +Lock-free caching improves throughput. +The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Valkey. +The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. + +Locking applies on the *cache level*, not per *cache entry*. + +It is possible to opt in to the locking behavior as follows: + +```java +ValkeyCacheManager cacheManager = ValkeyCacheManager + .builder(ValkeyCacheWriter.lockingValkeyCacheWriter(connectionFactory)) + .cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig()) + ... +``` + +By default, any `key` for a cache entry gets prefixed with the actual cache name followed by two colons. +This behavior can be changed to a static as well as a computed prefix. + +The following example shows how to set a static prefix: + +```java +// static key prefix +ValkeyCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)"); +``` + +The following example shows how to set a computed prefix: + +```java +// computed key prefix +ValkeyCacheConfiguration.defaultCacheConfig() + .computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName); +``` + +The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `KEYS` can cause performance issues with large keyspaces. +Therefore, the default `ValkeyCacheWriter` can be created with a `BatchStrategy` to switch to a `SCAN`-based batch strategy. +The `SCAN` strategy requires a batch size to avoid excessive Valkey command round trips: + +```java +ValkeyCacheManager cacheManager = ValkeyCacheManager + .builder(ValkeyCacheWriter.nonLockingValkeyCacheWriter(connectionFactory, BatchStrategies.scan(1000))) + .cacheDefaults(ValkeyCacheConfiguration.defaultCacheConfig()) + ... +``` + +:::note +The `KEYS` batch strategy is fully supported using any driver and Valkey operation mode (Standalone, Clustered). +::: + +`SCAN` is fully supported when using the Lettuce driver. +Jedis supports `SCAN` only in non-clustered modes. + +The following table lists the default settings for `ValkeyCacheManager`: + +*Table 1. `ValkeyCacheManager` defaults* + +| Setting | Value | +|---------|-------| +| Cache Writer | Non-locking, `KEYS` batch strategy | +| Cache Configuration | `ValkeyCacheConfiguration#defaultConfiguration` | +| Initial Caches | None | +| Transaction Aware | No | + +The following table lists the default settings for `ValkeyCacheConfiguration`: + +*Table 2. ValkeyCacheConfiguration defaults* + +| Setting | Value | +|---------|-------| +| Key Expiration | None | +| Cache `null` | Yes | +| Prefix Keys | Yes | +| Default Prefix | The actual cache name | +| Key Serializer | `StringValkeySerializer` | +| Value Serializer | `JdkSerializationValkeySerializer` | +| Conversion Service | `DefaultFormattingConversionService` with default cache key converters | + +:::note +By default `ValkeyCache`, statistics are disabled. +::: + +Use `ValkeyCacheManagerBuilder.enableStatistics()` to collect local _hits_ and _misses_ through `ValkeyCache#getStatistics()`, returning a snapshot of the collected data. + +## Valkey Cache Expiration + +The implementation of time-to-idle (TTI) as well as time-to-live (TTL) varies in definition and behavior even across different data stores. + +In general: + +* _time-to-live_ (TTL) _expiration_ - TTL is only set and reset by a create or update data access operation. +As long as the entry is written before the TTL expiration timeout, including on creation, an entry's timeout will reset to the configured duration of the TTL expiration timeout. +For example, if the TTL expiration timeout is set to 5 minutes, then the timeout will be set to 5 minutes on entry creation and reset to 5 minutes anytime the entry is updated thereafter and before the 5-minute interval expires. +If no update occurs within 5 minutes, even if the entry was read several times, or even just read once during the 5-minute interval, the entry will still expire. +The entry must be written to prevent the entry from expiring when declaring a TTL expiration policy. + +* _time-to-idle_ (TTI) _expiration_ - TTI is reset anytime the entry is also read as well as for entry updates, and is effectively and extension to the TTL expiration policy. + +:::note +Some data stores expire an entry when TTL is configured no matter what type of data access operation occurs on the entry (reads, writes, or otherwise). +After the set, configured TTL expiration timeout, the entry is evicted from the data store regardless. +Eviction actions (for example: destroy, invalidate, overflow-to-disk (for persistent stores), etc.) are data store specific. +::: + +### Time-To-Live (TTL) Expiration + +Spring Data Valkey's `Cache` implementation supports _time-to-live_ (TTL) expiration on cache entries. +Users can either configure the TTL expiration timeout with a fixed `Duration` or a dynamically computed `Duration` per cache entry by supplying an implementation of the new `ValkeyCacheWriter.TtlFunction` interface. + +:::tip +The `ValkeyCacheWriter.TtlFunction` interface was introduced in Spring Data Valkey `3.2.0`. +::: + +If all cache entries should expire after a set duration of time, then simply configure a TTL expiration timeout with a fixed `Duration`, as follows: + +```java +ValkeyCacheConfiguration fiveMinuteTtlExpirationDefaults = + ValkeyCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)); +``` + +However, if the TTL expiration timeout should vary by cache entry, then you must provide a custom implementation of the `ValkeyCacheWriter.TtlFunction` interface: + +```java +enum MyCustomTtlFunction implements TtlFunction { + + INSTANCE; + + @Override + public Duration getTimeToLive(Object key, @Nullable Object value) { + // compute a TTL expiration timeout (Duration) based on the cache entry key and/or value + } +} +``` + +:::note +Under-the-hood, a fixed `Duration` TTL expiration is wrapped in a `TtlFunction` implementation returning the provided `Duration`. +::: + +Then, you can either configure the fixed `Duration` or the dynamic, per-cache entry `Duration` TTL expiration on a global basis using: + +*Global fixed Duration TTL expiration timeout* + +```java +ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory) + .cacheDefaults(fiveMinuteTtlExpirationDefaults) + .build(); +``` + +Or, alternatively: + +*Global, dynamically computed per-cache entry Duration TTL expiration timeout* + +```java +ValkeyCacheConfiguration defaults = ValkeyCacheConfiguration.defaultCacheConfig() + .entryTtl(MyCustomTtlFunction.INSTANCE); + +ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory) + .cacheDefaults(defaults) + .build(); +``` + +Of course, you can combine both global and per-cache configuration using: + +*Global fixed Duration TTL expiration timeout* + +```java +ValkeyCacheConfiguration predefined = ValkeyCacheConfiguration.defaultCacheConfig() + .entryTtl(MyCustomTtlFunction.INSTANCE); + +Map initialCaches = Collections.singletonMap("predefined", predefined); + +ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(valkeyConnectionFactory) + .cacheDefaults(fiveMinuteTtlExpirationDefaults) + .withInitialCacheConfigurations(initialCaches) + .build(); +``` + +### Time-To-Idle (TTI) Expiration + +Valkey itself does not support the concept of true, time-to-idle (TTI) expiration. +Still, using Spring Data Valkey's Cache implementation, it is possible to achieve time-to-idle (TTI) expiration-like behavior. + +The configuration of TTI in Spring Data Valkey's Cache implementation must be explicitly enabled, that is, is opt-in. +Additionally, you must also provide TTL configuration using either a fixed `Duration` or a custom implementation of the `TtlFunction` interface as described above in [Valkey Cache Expiration](#valkey-cache-expiration). + +For example: + +```java +@Configuration +@EnableCaching +class ValkeyConfiguration { + + @Bean + ValkeyConnectionFactory valkeyConnectionFactory() { + // ... + } + + @Bean + ValkeyCacheManager cacheManager(ValkeyConnectionFactory connectionFactory) { + + ValkeyCacheConfiguration defaults = ValkeyCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(5)) + .enableTimeToIdle(); + + return ValkeyCacheManager.builder(connectionFactory) + .cacheDefaults(defaults) + .build(); + } +} +``` + +Because Valkey servers do not implement a proper notion of TTI, then TTI can only be achieved with Valkey commands accepting expiration options. +In Valkey, the "expiration" is technically a time-to-live (TTL) policy. +However, TTL expiration can be passed when reading the value of a key thereby effectively resetting the TTL expiration timeout, as is now the case in Spring Data Valkey's `Cache.get(key)` operation. + +`ValkeyCache.get(key)` is implemented by calling the Valkey `GETEX` command. + +:::danger +The Valkey [`GETEX`](https://valkey.io/commands/getex) command is only available in Valkey version `6.2.0` and later. +Therefore, if you are not using Valkey `6.2.0` or later, then it is not possible to use Spring Data Valkey's TTI expiration. +A command execution exception will be thrown if you enable TTI against an incompatible Valkey (server) version. +No attempt is made to determine if the Valkey server version is correct and supports the `GETEX` command. +::: + +:::danger +In order to achieve true time-to-idle (TTI) expiration-like behavior in your Spring Data Valkey application, then an entry must be consistently accessed with (TTL) expiration on every read or write operation. +There are no exceptions to this rule. +If you are mixing and matching different data access patterns across your Spring Data Valkey application (for example: caching, invoking operations using `ValkeyTemplate` and possibly, or especially when using Spring Data Repository CRUD operations), then accessing an entry may not necessarily prevent the entry from expiring if TTL expiration was set. +For example, an entry maybe "put" in (written to) the cache during a `@Cacheable` service method invocation with a TTL expiration (i.e. `SET `) and later read using a Spring Data Valkey Repository before the expiration timeout (using `GET` without expiration options). +A simple `GET` without specifying expiration options will not reset the TTL expiration timeout on an entry. +Therefore, the entry may expire before the next data access operation, even though it was just read. +Since this cannot be enforced in the Valkey server, then it is the responsibility of your application to consistently access an entry when time-to-idle expiration is configured, in and outside of caching, where appropriate. +::: diff --git a/docs/src/content/docs/redis/redis-repositories/anatomy.md b/docs/src/content/docs/valkey/valkey-repositories/anatomy.md similarity index 95% rename from docs/src/content/docs/redis/redis-repositories/anatomy.md rename to docs/src/content/docs/valkey/valkey-repositories/anatomy.md index 9aa1f0b22..f3a01bea6 100644 --- a/docs/src/content/docs/redis/redis-repositories/anatomy.md +++ b/docs/src/content/docs/valkey/valkey-repositories/anatomy.md @@ -1,9 +1,9 @@ --- -title: Redis Repositories Anatomy +title: Valkey Repositories Anatomy description: Anatomy documentation --- -Redis as a store itself offers a very narrow low-level API leaving higher level functions, such as secondary indexes and query operations, up to the user. +Valkey as a store itself offers a very narrow low-level API leaving higher level functions, such as secondary indexes and query operations, up to the user. This section provides a more detailed view of commands issued by the repository abstraction for a better understanding of potential performance implications. @@ -12,7 +12,7 @@ Consider the following entity class as the starting point for all operations: *Example 1. Example entity* ```java -@RedisHash("people") +@ValkeyHash("people") public class Person { @Id String id; diff --git a/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md new file mode 100644 index 000000000..e392b065c --- /dev/null +++ b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md @@ -0,0 +1,65 @@ +--- +title: CDI Integration +description: CDI Integration documentation +--- + +Instances of the repository interfaces are usually created by a container, for which Spring is the most natural choice when working with Spring Data. +Spring offers sophisticated support for creating bean instances. +Spring Data Valkey ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. +The extension is part of the JAR, so, to activate it, drop the Spring Data Valkey JAR into your classpath. + +You can then set up the infrastructure by implementing a CDI Producer for the `io.valkey.springframework.data.connection.ValkeyConnectionFactory` and `io.valkey.springframework.data.core.ValkeyOperations`, as shown in the following example: + +```java +class ValkeyOperationsProducer { + @Produces + ValkeyConnectionFactory valkeyConnectionFactory() { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new ValkeyStandaloneConfiguration()); + connectionFactory.afterPropertiesSet(); + connectionFactory.start(); + + return connectionFactory; + } + + void disposeValkeyConnectionFactory(@Disposes ValkeyConnectionFactory valkeyConnectionFactory) throws Exception { + + if (valkeyConnectionFactory instanceof DisposableBean) { + ((DisposableBean) valkeyConnectionFactory).destroy(); + } + } + + @Produces + @ApplicationScoped + ValkeyOperations valkeyOperationsProducer(ValkeyConnectionFactory valkeyConnectionFactory) { + + ValkeyTemplate template = new ValkeyTemplate(); + template.setConnectionFactory(valkeyConnectionFactory); + template.afterPropertiesSet(); + + return template; + } + +} +``` + +The necessary setup can vary, depending on your JavaEE environment. + +The Spring Data Valkey CDI extension picks up all available repositories as CDI beans and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. +Thus, obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property, as shown in the following example: + +```java +class RepositoryClient { + + @Inject + PersonRepository repository; + + public void businessMethod() { + List people = repository.findAll(); + } +} +``` + +A Valkey Repository requires `io.valkey.springframework.data.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.core.ValkeyKeyValueTemplate` instances. +These beans are created and managed by the Spring Data CDI extension if no provided beans are found. +You can, however, supply your own beans to configure the specific properties of `io.valkey.springframework.data.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.core.ValkeyKeyValueTemplate`. diff --git a/docs/src/content/docs/redis/redis-repositories/cluster.md b/docs/src/content/docs/valkey/valkey-repositories/cluster.md similarity index 77% rename from docs/src/content/docs/redis/redis-repositories/cluster.md rename to docs/src/content/docs/valkey/valkey-repositories/cluster.md index 56f0e66d3..2de64038c 100644 --- a/docs/src/content/docs/redis/redis-repositories/cluster.md +++ b/docs/src/content/docs/valkey/valkey-repositories/cluster.md @@ -1,10 +1,10 @@ --- -title: Redis Repositories Running on a Cluster +title: Valkey Repositories Running on a Cluster description: Cluster documentation --- -You can use the Redis repository support in a clustered Redis environment. -See the "[Redis Cluster](/redis/cluster)" section for `ConnectionFactory` configuration details. +You can use the Valkey repository support in a clustered Valkey environment. +See the "[Valkey Cluster](/valkey/cluster)" section for `ConnectionFactory` configuration details. Still, some additional configuration must be done, because the default key distribution spreads entities and secondary indexes through out the whole cluster and its slots. The following table shows the details of data on a cluster (based on previous examples): @@ -19,7 +19,7 @@ The following table shows the details of data on a cluster (based on previous ex Some commands (such as `SINTER` and `SUNION`) can only be processed on the server side when all involved keys map to the same slot. Otherwise, computation has to be done on client side. -Therefore, it is useful to pin keyspaces to a single slot, which lets make use of Redis server side computation right away. +Therefore, it is useful to pin keyspaces to a single slot, which lets make use of Valkey server side computation right away. The following table shows what happens when you do (note the change in the slot column and the port value in the node column): ## Pinned Keyspace Distribution @@ -31,5 +31,5 @@ The following table shows what happens when you do (note the change in the slot | {people}:firstname:rand | index | 2399 | 127.0.0.1:7379 | :::tip -Define and pin keyspaces by using `@RedisHash("{yourkeyspace}")` to specific slots when you use Redis cluster. +Define and pin keyspaces by using `@ValkeyHash("{yourkeyspace}")` to specific slots when you use Valkey cluster. ::: diff --git a/docs/src/content/docs/redis/redis-repositories/expirations.md b/docs/src/content/docs/valkey/valkey-repositories/expirations.md similarity index 54% rename from docs/src/content/docs/redis/redis-repositories/expirations.md rename to docs/src/content/docs/valkey/valkey-repositories/expirations.md index 9255d41d3..6e086add9 100644 --- a/docs/src/content/docs/redis/redis-repositories/expirations.md +++ b/docs/src/content/docs/valkey/valkey-repositories/expirations.md @@ -3,9 +3,9 @@ title: Time To Live description: Expirations documentation --- -Objects stored in Redis may be valid only for a certain amount of time. -This is especially useful for persisting short-lived objects in Redis without having to remove them manually when they reach their end of life. -The expiration time in seconds can be set with `@RedisHash(timeToLive=...)` as well as by using `org.springframework.data.redis.core.convert.KeyspaceConfiguration$KeyspaceSettings` (see [Keyspaces](/redis/redis-repositories/keyspaces)). +Objects stored in Valkey may be valid only for a certain amount of time. +This is especially useful for persisting short-lived objects in Valkey without having to remove them manually when they reach their end of life. +The expiration time in seconds can be set with `@ValkeyHash(timeToLive=...)` as well as by using `io.valkey.springframework.data.core.convert.KeyspaceConfiguration$KeyspaceSettings` (see [Keyspaces](/valkey/valkey-repositories/keyspaces)). More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or a method. However, do not apply `@TimeToLive` on both a method and a property within the same class. @@ -36,30 +36,30 @@ public class TimeToLiveOnMethod { ``` :::note -Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no associated expiration. +Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Valkey. -1 indicates that the object has no associated expiration. ::: -The repository implementation ensures subscription to [Redis keyspace notifications](https://redis.io/topics/notifications) via `org.springframework.data.redis.listener.RedisMessageListenerContainer`. +The repository implementation ensures subscription to [Valkey keyspace notifications](https://valkey.io/topics/notifications) via `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer`. When the expiration is set to a positive value, the corresponding `EXPIRE` command is run. -In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. -This is done to enable the Repository support to publish `org.springframework.data.redis.core.RedisKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. -Expiry events are received on all connected applications that use Spring Data Redis repositories. +In addition to persisting the original, a phantom copy is persisted in Valkey and set to expire five minutes after the original one. +This is done to enable the Repository support to publish `io.valkey.springframework.data.core.ValkeyKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. +Expiry events are received on all connected applications that use Spring Data Valkey repositories. By default, the key expiry listener is disabled when initializing the application. -The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. -See `org.springframework.data.redis.core.RedisKeyValueAdapter$EnableKeyspaceEvents` for possible values. +The startup mode can be adjusted in `@EnableValkeyRepositories` or `ValkeyKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. +See `io.valkey.springframework.data.core.ValkeyKeyValueAdapter$EnableKeyspaceEvents` for possible values. -The `RedisKeyExpiredEvent` holds a copy of the expired domain object as well as the key. +The `ValkeyKeyExpiredEvent` holds a copy of the expired domain object as well as the key. :::note -Delaying or disabling the expiry event listener startup impacts `RedisKeyExpiredEvent` publishing. +Delaying or disabling the expiry event listener startup impacts `ValkeyKeyExpiredEvent` publishing. A disabled event listener does not publish expiry events. A delayed startup can cause loss of events because of the delayed listener initialization. ::: :::note -The keyspace notification message listener alters `notify-keyspace-events` settings in Redis, if those are not already set. +The keyspace notification message listener alters `notify-keyspace-events` settings in Valkey, if those are not already set. Existing settings are not overridden, so you must set up those settings correctly (or leave them empty). Note that `CONFIG` is disabled on AWS ElastiCache, and enabling the listener leads to an error. To work around this behavior, set the `keyspaceNotificationsConfigParameter` parameter to an empty string. @@ -67,10 +67,10 @@ This prevents `CONFIG` command usage. ::: :::note -Redis Pub/Sub messages are not persistent. +Valkey Pub/Sub messages are not persistent. If a key expires while the application is down, the expiry event is not processed, which may lead to secondary indexes containing references to the expired object. ::: :::note -`@EnableKeyspaceEvents(shadowCopy = OFF)` disable storage of phantom copies and reduces data size within Redis. `RedisKeyExpiredEvent` will only contain the `id` of the expired key. +`@EnableKeyspaceEvents(shadowCopy = OFF)` disable storage of phantom copies and reduces data size within Valkey. `ValkeyKeyExpiredEvent` will only contain the `id` of the expired key. ::: diff --git a/docs/src/content/docs/redis/redis-repositories/indexes.md b/docs/src/content/docs/valkey/valkey-repositories/indexes.md similarity index 81% rename from docs/src/content/docs/redis/redis-repositories/indexes.md rename to docs/src/content/docs/valkey/valkey-repositories/indexes.md index 170d31672..882646bb6 100644 --- a/docs/src/content/docs/redis/redis-repositories/indexes.md +++ b/docs/src/content/docs/valkey/valkey-repositories/indexes.md @@ -3,8 +3,8 @@ title: Secondary Indexes description: Indexes documentation --- -[Secondary indexes](https://redis.io/topics/indexes) are used to enable lookup operations based on native Redis structures. -Values are written to the according indexes on every save and are removed when objects are deleted or [expire](/redis/redis-repositories/expirations). +[Secondary indexes](https://valkey.io/topics/indexes) are used to enable lookup operations based on native Valkey structures. +Values are written to the according indexes on every save and are removed when objects are deleted or [expire](/valkey/valkey-repositories/expirations). ## Simple Property Index @@ -13,7 +13,7 @@ Given the sample `Person` entity shown earlier, we can create an index for `firs *Example 1. Annotation driven indexing* ```java -@RedisHash("people") +@ValkeyHash("people") public class Person { @Id String id; @@ -42,7 +42,7 @@ SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e Furthermore, the programmatic setup lets you define indexes on map keys and list properties, as shown in the following example: ```java -@RedisHash("people") +@ValkeyHash("people") public class Person { // ... other properties omitted @@ -59,19 +59,19 @@ public class Person { ``` :::caution -Indexes cannot be resolved on [References](/redis/redis-repositories/usage#redis.repositories.references). +Indexes cannot be resolved on [References](/valkey/valkey-repositories/usage#valkey.repositories.references). ::: As with keyspaces, you can configure indexes without needing to annotate the actual domain type, as shown in the following example: -*Example 2. Index Setup with @EnableRedisRepositories* +*Example 2. Index Setup with @EnableValkeyRepositories* ```java @Configuration -@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class) +@EnableValkeyRepositories(indexConfiguration = MyIndexConfiguration.class) public class ApplicationConfig { - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + //... ValkeyConnectionFactory and ValkeyTemplate Bean definitions omitted public static class MyIndexConfiguration extends IndexConfiguration { @@ -89,14 +89,14 @@ Again, as with keyspaces, you can programmatically configure indexes, as shown i ```java @Configuration -@EnableRedisRepositories +@EnableValkeyRepositories public class ApplicationConfig { - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + //... ValkeyConnectionFactory and ValkeyTemplate Bean definitions omitted @Bean - public RedisMappingContext keyValueMappingContext() { - return new RedisMappingContext( + public ValkeyMappingContext keyValueMappingContext() { + return new ValkeyMappingContext( new MappingConfiguration( new KeyspaceConfiguration(), new MyIndexConfiguration())); } @@ -114,10 +114,10 @@ public class ApplicationConfig { ## Geospatial Index Assume the `Address` type contains a `location` property of type `Point` that holds the geo coordinates of the particular address. -By annotating the property with `@GeoIndexed`, Spring Data Redis adds those values by using Redis `GEO` commands, as shown in the following example: +By annotating the property with `@GeoIndexed`, Spring Data Valkey adds those values by using Valkey `GEO` commands, as shown in the following example: ```java -@RedisHash("people") +@ValkeyHash("people") public class Person { Address address; diff --git a/docs/src/content/docs/redis/redis-repositories/keyspaces.md b/docs/src/content/docs/valkey/valkey-repositories/keyspaces.md similarity index 65% rename from docs/src/content/docs/redis/redis-repositories/keyspaces.md rename to docs/src/content/docs/valkey/valkey-repositories/keyspaces.md index da94cc3cc..2d5453b64 100644 --- a/docs/src/content/docs/redis/redis-repositories/keyspaces.md +++ b/docs/src/content/docs/valkey/valkey-repositories/keyspaces.md @@ -3,21 +3,21 @@ title: Keyspaces description: Keyspaces documentation --- -Keyspaces define prefixes used to create the actual key for the Redis Hash. +Keyspaces define prefixes used to create the actual key for the Valkey Hash. By default, the prefix is set to `getClass().getName()`. -You can alter this default by setting `@RedisHash` on the aggregate root level or by setting up a programmatic configuration. +You can alter this default by setting `@ValkeyHash` on the aggregate root level or by setting up a programmatic configuration. However, the annotated keyspace supersedes any other configuration. -The following example shows how to set the keyspace configuration with the `@EnableRedisRepositories` annotation: +The following example shows how to set the keyspace configuration with the `@EnableValkeyRepositories` annotation: -*Example 1. Keyspace Setup via `@EnableRedisRepositories`* +*Example 1. Keyspace Setup via `@EnableValkeyRepositories`* ```java @Configuration -@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class) +@EnableValkeyRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class) public class ApplicationConfig { - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + //... ValkeyConnectionFactory and ValkeyTemplate Bean definitions omitted public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { @@ -35,14 +35,14 @@ The following example shows how to programmatically set the keyspace: ```java @Configuration -@EnableRedisRepositories +@EnableValkeyRepositories public class ApplicationConfig { - //... RedisConnectionFactory and RedisTemplate Bean definitions omitted + //... ValkeyConnectionFactory and ValkeyTemplate Bean definitions omitted @Bean - public RedisMappingContext keyValueMappingContext() { - return new RedisMappingContext( + public ValkeyMappingContext keyValueMappingContext() { + return new ValkeyMappingContext( new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration())); } diff --git a/docs/src/content/docs/redis/redis-repositories/mapping.md b/docs/src/content/docs/valkey/valkey-repositories/mapping.md similarity index 75% rename from docs/src/content/docs/redis/redis-repositories/mapping.md rename to docs/src/content/docs/valkey/valkey-repositories/mapping.md index 04ce1985d..dfdaddaef 100644 --- a/docs/src/content/docs/redis/redis-repositories/mapping.md +++ b/docs/src/content/docs/valkey/valkey-repositories/mapping.md @@ -3,9 +3,9 @@ title: Object-to-Hash Mapping description: Mapping documentation --- -The Redis Repository support persists Objects to Hashes. -This requires an Object-to-Hash conversion which is done by a `RedisConverter`. -The default implementation uses `Converter` for mapping property values to and from Redis native `byte[]`. +The Valkey Repository support persists Objects to Hashes. +This requires an Object-to-Hash conversion which is done by a `ValkeyConverter`. +The default implementation uses `Converter` for mapping property values to and from Valkey native `byte[]`. Given the `Person` type from the previous sections, the default mapping looks like the following: @@ -43,13 +43,13 @@ This section explains how types are mapped to and from a Hash representation: Due to the flat representation structure, Map keys need to be simple types, such as `String` or `Number`. ::: -Mapping behavior can be customized by registering the corresponding `Converter` in `RedisCustomConversions`. +Mapping behavior can be customized by registering the corresponding `Converter` in `ValkeyCustomConversions`. Those converters can take care of converting from and to a single `byte[]` as well as `Map`. The first one is suitable for (for example) converting a complex type to (for example) a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash. :::danger -Writing objects to a Redis hash deletes the content from the hash and re-creates the whole hash, so data that has not been mapped is lost. +Writing objects to a Valkey hash deletes the content from the hash and re-creates the whole hash, so data that has not been mapped is lost. ::: The following example shows two sample byte array converters: @@ -60,11 +60,11 @@ The following example shows two sample byte array converters: @WritingConverter public class AddressToBytesConverter implements Converter { - private final Jackson2JsonRedisSerializer
serializer; + private final Jackson2JsonValkeySerializer
serializer; public AddressToBytesConverter() { - serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer = new Jackson2JsonValkeySerializer
(Address.class); serializer.setObjectMapper(new ObjectMapper()); } @@ -77,11 +77,11 @@ public class AddressToBytesConverter implements Converter { @ReadingConverter public class BytesToAddressConverter implements Converter { - private final Jackson2JsonRedisSerializer
serializer; + private final Jackson2JsonValkeySerializer
serializer; public BytesToAddressConverter() { - serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer = new Jackson2JsonValkeySerializer
(Address.class); serializer.setObjectMapper(new ObjectMapper()); } @@ -137,14 +137,14 @@ ciudad = "emond's field" ``` :::note -Custom conversions have no effect on index resolution. [Secondary Indexes](/redis/redis-repositories/indexes) are still created, even for custom converted types. +Custom conversions have no effect on index resolution. [Secondary Indexes](/valkey/valkey-repositories/indexes) are still created, even for custom converted types. ::: ## Customizing Type Mapping If you want to avoid writing the entire Java class name as type information and would rather like to use a key, you can use the `@TypeAlias` annotation on the entity class being persisted. If you need to customize the mapping even more, look at the [`TypeInformationMapper`](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/convert/TypeInformationMapper.html) interface. -An instance of that interface can be configured at the `DefaultRedisTypeMapper`, which can be configured on `MappingRedisConverter`. +An instance of that interface can be configured at the `DefaultValkeyTypeMapper`, which can be configured on `MappingValkeyConverter`. The following example shows how to define a type alias for an entity: @@ -161,35 +161,35 @@ The resulting document contains `pers` as the value in a `_class` field. ### Configuring Custom Type Mapping -The following example demonstrates how to configure a custom `RedisTypeMapper` in `MappingRedisConverter`: +The following example demonstrates how to configure a custom `ValkeyTypeMapper` in `MappingValkeyConverter`: -*Example 4. Configuring a custom `RedisTypeMapper` via Spring Java Config* +*Example 4. Configuring a custom `ValkeyTypeMapper` via Spring Java Config* ```java -class CustomRedisTypeMapper extends DefaultRedisTypeMapper { +class CustomValkeyTypeMapper extends DefaultValkeyTypeMapper { //implement custom type mapping here } ``` ```java @Configuration -class SampleRedisConfiguration { +class SampleValkeyConfiguration { @Bean - public MappingRedisConverter redisConverter(RedisMappingContext mappingContext, - RedisCustomConversions customConversions, ReferenceResolver referenceResolver) { + public MappingValkeyConverter valkeyConverter(ValkeyMappingContext mappingContext, + ValkeyCustomConversions customConversions, ReferenceResolver referenceResolver) { - MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver, + MappingValkeyConverter mappingValkeyConverter = new MappingValkeyConverter(mappingContext, null, referenceResolver, customTypeMapper()); - mappingRedisConverter.setCustomConversions(customConversions); + mappingValkeyConverter.setCustomConversions(customConversions); - return mappingRedisConverter; + return mappingValkeyConverter; } @Bean - public RedisTypeMapper customTypeMapper() { - return new CustomRedisTypeMapper(); + public ValkeyTypeMapper customTypeMapper() { + return new CustomValkeyTypeMapper(); } } ``` diff --git a/docs/src/content/docs/redis/redis-repositories/queries.md b/docs/src/content/docs/valkey/valkey-repositories/queries.md similarity index 59% rename from docs/src/content/docs/redis/redis-repositories/queries.md rename to docs/src/content/docs/valkey/valkey-repositories/queries.md index c512465a6..d95001476 100644 --- a/docs/src/content/docs/redis/redis-repositories/queries.md +++ b/docs/src/content/docs/valkey/valkey-repositories/queries.md @@ -1,5 +1,5 @@ --- -title: Redis-specific Query Methods +title: Valkey-specific Query Methods description: Queries documentation --- @@ -19,30 +19,30 @@ Please make sure properties used in finder methods are set up for indexing. ::: :::note -Query methods for Redis repositories support only queries for entities and collections of entities with paging. +Query methods for Valkey repositories support only queries for entities and collections of entities with paging. ::: -Using derived query methods might not always be sufficient to model the queries to run. `RedisCallback` offers more control over the actual matching of index structures or even custom indexes. -To do so, provide a `RedisCallback` that returns a single or `Iterable` set of `id` values, as shown in the following example: +Using derived query methods might not always be sufficient to model the queries to run. `ValkeyCallback` offers more control over the actual matching of index structures or even custom indexes. +To do so, provide a `ValkeyCallback` that returns a single or `Iterable` set of `id` values, as shown in the following example: -*Example 2. Sample finder using RedisCallback* +*Example 2. Sample finder using ValkeyCallback* ```java String user = //... -List sessionsByUser = template.find(new RedisCallback>() { +List sessionsByUser = template.find(new ValkeyCallback>() { - public Set doInRedis(RedisConnection connection) throws DataAccessException { + public Set doInValkey(ValkeyConnection connection) throws DataAccessException { return connection .sMembers("sessions:securityContext.authentication.principal.username:" + user); - }}, RedisSession.class); + }}, ValkeySession.class); ``` -The following table provides an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to: +The following table provides an overview of the keywords supported for Valkey and what a method containing that keyword essentially translates to: *Table 1. Supported keywords inside method names* -| Keyword | Sample | Redis snippet | +| Keyword | Sample | Valkey snippet | |---------|--------|---------------| | `And` | `findByLastnameAndFirstname` | `SINTER …:firstname:rand …:lastname:al'thor` | | `Or` | `findByLastnameOrFirstname` | `SUNION …:firstname:rand …:lastname:al'thor` | @@ -53,15 +53,15 @@ The following table provides an overview of the keywords supported for Redis and ## Sorting Query Method results -Redis repositories allow various approaches to define sorting order. -Redis itself does not support in-flight sorting when retrieving hashes or sets. -Therefore, Redis repository query methods construct a `Comparator` that is applied to the result before returning results as `List`. +Valkey repositories allow various approaches to define sorting order. +Valkey itself does not support in-flight sorting when retrieving hashes or sets. +Therefore, Valkey repository query methods construct a `Comparator` that is applied to the result before returning results as `List`. Let's take a look at the following example: *Example 3. Sorting Query Results* ```java -interface PersonRepository extends RedisRepository { +interface PersonRepository extends ValkeyRepository { List findByFirstnameOrderByAgeDesc(String firstname); // (1) diff --git a/docs/src/content/docs/redis/redis-repositories/query-by-example.md b/docs/src/content/docs/valkey/valkey-repositories/query-by-example.md similarity index 97% rename from docs/src/content/docs/redis/redis-repositories/query-by-example.md rename to docs/src/content/docs/valkey/valkey-repositories/query-by-example.md index 16c034501..f9a30ba96 100644 --- a/docs/src/content/docs/redis/redis-repositories/query-by-example.md +++ b/docs/src/content/docs/valkey/valkey-repositories/query-by-example.md @@ -1,6 +1,6 @@ --- title: Query by Example -description: Query by Example (QBE) for Redis repositories including usage, matchers, and Redis-specific limitations +description: Query by Example (QBE) for Valkey repositories including usage, matchers, and Valkey-specific limitations --- ## Introduction @@ -216,7 +216,7 @@ class PersonService { } ``` -Redis Repositories support, with their secondary indexes, a subset of Spring Data's Query by Example features. +Valkey Repositories support, with their secondary indexes, a subset of Spring Data's Query by Example features. In particular, only exact, case-sensitive, and non-null values are used to construct a query. Secondary indexes use set-based operations (Set intersection, Set union) to determine matching keys. Adding a property to the query that is not indexed returns no result, because no index exists. Query by Example support inspects indexing configuration to include only properties in the query that are covered by an index. This is to prevent accidental inclusion of non-indexed properties. diff --git a/docs/src/content/docs/redis/redis-repositories/usage.md b/docs/src/content/docs/valkey/valkey-repositories/usage.md similarity index 79% rename from docs/src/content/docs/redis/redis-repositories/usage.md rename to docs/src/content/docs/valkey/valkey-repositories/usage.md index d54e9d669..0c717f764 100644 --- a/docs/src/content/docs/redis/redis-repositories/usage.md +++ b/docs/src/content/docs/valkey/valkey-repositories/usage.md @@ -3,12 +3,12 @@ title: Usage description: Usage documentation --- -Spring Data Redis lets you easily implement domain entities, as shown in the following example: +Spring Data Valkey lets you easily implement domain entities, as shown in the following example: *Example 1. Sample Person Entity* ```java -@RedisHash("people") +@ValkeyHash("people") public class Person { @Id String id; @@ -19,7 +19,7 @@ public class Person { ``` We have a pretty simple domain object here. -Note that it has a `@RedisHash` annotation on its type and a property named `id` that is annotated with `org.springframework.data.annotation.Id`. +Note that it has a `@ValkeyHash` annotation on its type and a property named `id` that is annotated with `org.springframework.data.annotation.Id`. Those two items are responsible for creating the actual key used to persist the hash. :::note @@ -40,23 +40,23 @@ public interface PersonRepository extends CrudRepository { As our repository extends `CrudRepository`, it provides basic CRUD and finder operations. The thing we need in between to glue things together is the corresponding Spring configuration, shown in the following example: -*Example 3. JavaConfig for Redis Repositories* +*Example 3. JavaConfig for Valkey Repositories* ```java @Configuration -@EnableRedisRepositories +@EnableValkeyRepositories public class ApplicationConfig { @Bean - public RedisConnectionFactory connectionFactory() { + public ValkeyConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public ValkeyTemplate valkeyTemplate(ValkeyConnectionFactory valkeyConnectionFactory) { - RedisTemplate template = new RedisTemplate(); - template.setConnectionFactory(redisConnectionFactory); + ValkeyTemplate template = new ValkeyTemplate(); + template.setConnectionFactory(valkeyConnectionFactory); return template; } } @@ -84,16 +84,16 @@ public void basicCrudOperations() { } ``` ```text -1. Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Redis Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2025-beeb-c666fab4c0e5`. +1. Generates a new `id` if the current value is `null` or reuses an already set `id` value and stores properties of type `Person` inside the Valkey Hash with a key that has a pattern of `keyspace:id` -- in this case, it might be `people:5d67b7e1-8640-2025-beeb-c666fab4c0e5`. 2. Uses the provided `id` to retrieve the object stored at `keyspace:id`. -3. Counts the total number of entities available within the keyspace, `people`, defined by `@RedisHash` on `Person`. -4. Removes the key for the given object from Redis. +3. Counts the total number of entities available within the keyspace, `people`, defined by `@ValkeyHash` on `Person`. +4. Removes the key for the given object from Valkey. ``` ## Persisting References Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself. -On loading from Redis, references are resolved automatically and mapped back into the object, as shown in the following example: +On loading from Valkey, references are resolved automatically and mapped back into the object, as shown in the following example: *Example 5. Sample Property Reference* @@ -150,9 +150,9 @@ This does not work when a custom conversion is registered. 3. Remove the `age` property. 4. Set complex `address` property. 5. Set a map of values, which removes the previously existing map and replaces the values with the given ones. -6. Automatically update the server expiration time when altering [Time To Live](/redis/redis-repositories/expirations). +6. Automatically update the server expiration time when altering [Time To Live](/valkey/valkey-repositories/expirations). ``` :::note -Updating complex objects as well as map (or other collection) structures requires further interaction with Redis to determine existing values, which means that rewriting the entire entity might be faster. +Updating complex objects as well as map (or other collection) structures requires further interaction with Valkey to determine existing values, which means that rewriting the entire entity might be faster. ::: diff --git a/docs/src/content/docs/redis/redis-streams.md b/docs/src/content/docs/valkey/valkey-streams.md similarity index 73% rename from docs/src/content/docs/redis/redis-streams.md rename to docs/src/content/docs/valkey/valkey-streams.md index e31ffb1bb..89097390c 100644 --- a/docs/src/content/docs/redis/redis-streams.md +++ b/docs/src/content/docs/valkey/valkey-streams.md @@ -1,38 +1,38 @@ --- -title: Redis Streams -description: Redis Streams documentation +title: Valkey Streams +description: Valkey Streams documentation --- -Redis Streams model a log data structure in an abstract approach. Typically, logs are append-only data structures and are consumed from the beginning on, at a random position, or by streaming new messages. +Valkey Streams model a log data structure in an abstract approach. Typically, logs are append-only data structures and are consumed from the beginning on, at a random position, or by streaming new messages. :::note -Learn more about Redis Streams in the [Redis reference documentation](https://redis.io/topics/streams-intro). +Learn more about Valkey Streams in the [Valkey reference documentation](https://valkey.io/topics/streams-intro). ::: -Redis Streams can be roughly divided into two areas of functionality: +Valkey Streams can be roughly divided into two areas of functionality: * Appending records * Consuming records -Although this pattern has similarities to [Pub/Sub](/redis/pubsub), the main difference lies in the persistence of messages and how they are consumed. +Although this pattern has similarities to [Pub/Sub](/valkey/pubsub), the main difference lies in the persistence of messages and how they are consumed. -While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don't listen, you miss a message), Redis Stream use a persistent, append-only data type that retains messages until the stream is trimmed. Another difference in consumption is that Pub/Sub registers a server-side subscription. Redis pushes arriving messages to the client while Redis Streams require active polling. +While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don't listen, you miss a message), Valkey Stream use a persistent, append-only data type that retains messages until the stream is trimmed. Another difference in consumption is that Pub/Sub registers a server-side subscription. Valkey pushes arriving messages to the client while Valkey Streams require active polling. -The `org.springframework.data.redis.connection` and `org.springframework.data.redis.stream` packages provide the core functionality for Redis Streams. +The `io.valkey.springframework.data.connection` and `io.valkey.springframework.data.stream` packages provide the core functionality for Valkey Streams. ## Appending -To send a record, you can use, as with the other operations, either the low-level `RedisConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `RedisConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as records, as shown in the following example: +To send a record, you can use, as with the other operations, either the low-level `ValkeyConnection` or the high-level `StreamOperations`. Both entities offer the `add` (`xAdd`) method, which accepts the record and the destination stream as arguments. While `ValkeyConnection` requires raw data (array of bytes), the `StreamOperations` lets arbitrary objects be passed in as records, as shown in the following example: ```java // append message through connection -RedisConnection con = … +ValkeyConnection con = … byte[] stream = … ByteRecord record = StreamRecords.rawBytes(…).withStreamKey(stream); con.xAdd(record); -// append message through RedisTemplate -RedisTemplate template = … +// append message through ValkeyTemplate +ValkeyTemplate template = … StringRecord record = StreamRecords.string(…).withStreamKey("my-stream"); template.opsForStream().add(record); ``` @@ -41,12 +41,12 @@ Stream records carry a `Map`, key-value tuples, as their payload. Appending a re ## Consuming -On the consuming side, one can consume one or multiple streams. Redis Streams provide read commands that allow consumption of the stream from an arbitrary position (random access) within the known stream content and beyond the stream end to consume new stream record. +On the consuming side, one can consume one or multiple streams. Valkey Streams provide read commands that allow consumption of the stream from an arbitrary position (random access) within the known stream content and beyond the stream end to consume new stream record. -At the low-level, `RedisConnection` offers the `xRead` and `xReadGroup` methods that map the Redis commands for reading and reading within a consumer group, respectively. Note that multiple streams can be used as arguments. +At the low-level, `ValkeyConnection` offers the `xRead` and `xReadGroup` methods that map the Valkey commands for reading and reading within a consumer group, respectively. Note that multiple streams can be used as arguments. :::note -Subscription commands in Redis can be blocking. That is, calling `xRead` on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the read command times out or receives a message. +Subscription commands in Valkey can be blocking. That is, calling `xRead` on a connection causes the current thread to block as it starts waiting for messages. The thread is released only if the read command times out or receives a message. ::: To consume stream messages, one can either poll for messages in application code, or use one of the two [Asynchronous reception through Message Listener Containers](#asynchronous-reception-through-message-listener-containers), the imperative or the reactive one. Each time a new records arrives, the container notifies the application code. @@ -56,8 +56,8 @@ To consume stream messages, one can either poll for messages in application code While stream consumption is typically associated with asynchronous processing, it is possible to consume messages synchronously. The overloaded `StreamOperations.read(…)` methods provide this functionality. During a synchronous receive, the calling thread potentially blocks until a message becomes available. The property `StreamReadOptions.block` specifies how long the receiver should wait before giving up waiting for a message. ```java -// Read message through RedisTemplate -RedisTemplate template = … +// Read message through ValkeyTemplate +ValkeyTemplate template = … List> messages = template.opsForStream().read(StreamReadOptions.empty().count(2), StreamOffset.latest("my-stream")); @@ -73,16 +73,16 @@ Due to its blocking nature, low-level polling is not attractive, as it requires Spring Data ships with two implementations tailored to the used programming model: -* `org.springframework.data.redis.stream.StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Redis Stream and drive the `org.springframework.data.redis.stream.StreamListener` instances that are injected into it. -* `org.springframework.data.redis.stream.StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Redis Stream as potentially infinite stream and emit stream messages through a `Flux`. +* `io.valkey.springframework.data.stream.StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Valkey Stream and drive the `io.valkey.springframework.data.stream.StreamListener` instances that are injected into it. +* `io.valkey.springframework.data.stream.StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Valkey Stream as potentially infinite stream and emit stream messages through a `Flux`. -`StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Redis infrastructure concerns to the framework. +`StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Valkey infrastructure concerns to the framework. -Both containers allow runtime configuration changes so that you can add or remove subscriptions while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `RedisConnection` only when needed. If all the listeners are unsubscribed, it automatically performs a cleanup, and the thread is released. +Both containers allow runtime configuration changes so that you can add or remove subscriptions while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `ValkeyConnection` only when needed. If all the listeners are unsubscribed, it automatically performs a cleanup, and the thread is released. #### Imperative `StreamMessageListenerContainer` -In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `org.springframework.data.redis.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. +In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `io.valkey.springframework.data.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. ```java class ExampleStreamListener implements StreamListener> { @@ -111,7 +111,7 @@ message -> { Once you've implemented your `StreamListener`, it's time to create a message listener container and register a subscription: ```java -RedisConnectionFactory connectionFactory = … +ValkeyConnectionFactory connectionFactory = … StreamListener> streamListener = … StreamMessageListenerContainerOptions> containerOptions = StreamMessageListenerContainerOptions @@ -142,7 +142,7 @@ return messages.doOnNext(it -> { Now we need to create the `StreamReceiver` and register a subscription to consume stream messages: ```java -ReactiveRedisConnectionFactory connectionFactory = … +ReactiveValkeyConnectionFactory connectionFactory = … StreamReceiverOptions> options = StreamReceiverOptions.builder().pollTimeout(Duration.ofMillis(100)) .build(); @@ -170,7 +170,7 @@ container.receive(Consumer.from("my-group", "my-consumer"), // (1) msg -> { // ... - redisTemplate.opsForStream().acknowledge("my-group", msg); // (2) + valkeyTemplate.opsForStream().acknowledge("my-group", msg); // (2) }); ``` ```text @@ -184,7 +184,7 @@ To auto acknowledge messages on receive use `receiveAutoAck` instead of `receive ### `ReadOffset` strategies -Stream read operations accept a read offset specification to consume messages from the given offset on. `ReadOffset` represents the read offset specification. Redis supports 3 variants of offsets, depending on whether you consume the stream standalone or within a consumer group: +Stream read operations accept a read offset specification to consume messages from the given offset on. `ReadOffset` represents the read offset specification. Valkey supports 3 variants of offsets, depending on whether you consume the stream standalone or within a consumer group: * `ReadOffset.latest()` – Read the latest message. * `ReadOffset.from(…)` – Read after a specific message Id. @@ -205,7 +205,7 @@ Using the latest message for read can skip messages that were added to the strea ## Serialization -Any Record sent to the stream needs to be serialized to its binary format. Due to the streams closeness to the hash data structure the stream key, field names and values use the according serializers configured on the `RedisTemplate`. +Any Record sent to the stream needs to be serialized to its binary format. Due to the streams closeness to the hash data structure the stream key, field names and values use the according serializers configured on the `ValkeyTemplate`. *Table 2. Stream Serialization* @@ -215,7 +215,7 @@ Any Record sent to the stream needs to be serialized to its binary format. Due t | field | hashKeySerializer | used for each map key in the payload | | value | hashValueSerializer | used for each map value in the payload | -Please make sure to review `RedisSerializer`s in use and note that if you decide to not use any serializer you need to make sure those values are binary already. +Please make sure to review `ValkeySerializer`s in use and note that if you decide to not use any serializer you need to make sure those values are binary already. ## Object Mapping @@ -229,11 +229,11 @@ ObjectRecord record = StreamRecords.newRecord() .in("my-stream") .ofObject("my-value"); -redisTemplate() +valkeyTemplate() .opsForStream() .add(record); // (1) -List> records = redisTemplate() +List> records = valkeyTemplate() .opsForStream() .read(String.class, StreamOffset.fromStart("my-stream")); ``` @@ -248,15 +248,15 @@ List> records = redisTemplate() Adding a complex value to the stream can be done in 3 ways: * Convert to simple value using e. g. a String JSON representation. -* Serialize the value with a suitable `RedisSerializer`. -* Convert the value into a `Map` suitable for serialization using a [`HashMapper`](/redis/hash-mappers). +* Serialize the value with a suitable `ValkeySerializer`. +* Convert the value into a `Map` suitable for serialization using a [`HashMapper`](/valkey/hash-mappers). The first variant is the most straight forward one but neglects the field value capabilities offered by the stream structure, still the values in the stream will be readable for other consumers. The 2nd option holds the same benefits as the first one, but may lead to a very specific consumer limitations as the all consumers must implement the very same serialization mechanism. The `HashMapper` approach is the a bit more complex one making use of the steams hash structure, but flattening the source. Still other consumers remain able to read the records as long as suitable serializer combinations are chosen. :::note -[HashMappers](/redis/hash-mappers) convert the payload to a `Map` with specific types. Make sure to use Hash-Key and Hash-Value serializers that are capable of (de-)serializing the hash. +[HashMappers](/valkey/hash-mappers) convert the payload to a `Map` with specific types. Make sure to use Hash-Key and Hash-Value serializers that are capable of (de-)serializing the hash. ::: ```java @@ -264,11 +264,11 @@ ObjectRecord record = StreamRecords.newRecord() .in("user-logon") .ofObject(new User("night", "angel")); -redisTemplate() +valkeyTemplate() .opsForStream() .add(record); // (1) -List> records = redisTemplate() +List> records = valkeyTemplate() .opsForStream() .read(User.class, StreamOffset.fromStart("user-logon")); ``` @@ -276,11 +276,11 @@ List> records = redisTemplate() 1. XADD user-logon * "_class" "com.example.User" "firstname" "night" "lastname" "angel" ``` -`StreamOperations` use by default [ObjectHashMapper](/redis/redis-repositories/mapping). +`StreamOperations` use by default [ObjectHashMapper](/valkey/valkey-repositories/mapping). You may provide a `HashMapper` suitable for your requirements when obtaining `StreamOperations`. ```java -redisTemplate() +valkeyTemplate() .opsForStream(new Jackson2HashMapper(true)) .add(record); // (1) ``` @@ -290,29 +290,29 @@ redisTemplate() :::note A `StreamMessageListenerContainer` may not be aware of any `@TypeAlias` used on domain types as those need to be resolved through a `MappingContext`. -Make sure to initialize `RedisMappingContext` with a `initialEntitySet`. +Make sure to initialize `ValkeyMappingContext` with a `initialEntitySet`. ::: ```java @Bean -RedisMappingContext redisMappingContext() { - RedisMappingContext ctx = new RedisMappingContext(); +ValkeyMappingContext valkeyMappingContext() { + ValkeyMappingContext ctx = new ValkeyMappingContext(); ctx.setInitialEntitySet(Collections.singleton(Person.class)); return ctx; } @Bean -RedisConverter redisConverter(RedisMappingContext mappingContext) { - return new MappingRedisConverter(mappingContext); +ValkeyConverter valkeyConverter(ValkeyMappingContext mappingContext) { + return new MappingValkeyConverter(mappingContext); } @Bean -ObjectHashMapper hashMapper(RedisConverter converter) { +ObjectHashMapper hashMapper(ValkeyConverter converter) { return new ObjectHashMapper(converter); } @Bean -StreamMessageListenerContainer streamMessageListenerContainer(RedisConnectionFactory connectionFactory, ObjectHashMapper hashMapper) { +StreamMessageListenerContainer streamMessageListenerContainer(ValkeyConnectionFactory connectionFactory, ObjectHashMapper hashMapper) { StreamMessageListenerContainerOptions> options = StreamMessageListenerContainerOptions.builder() .objectMapper(hashMapper) .build(); From 9cba22114907d4a631c9d136f7cc981f6d1cb928 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 28 Jan 2026 09:40:36 -0800 Subject: [PATCH 04/21] Update migration docs for Valkey Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 3 +- docs/src/content/docs/commons/migration.md | 119 ++------------------- docs/src/content/docs/commons/upgrade.md | 14 --- docs/src/content/docs/index.mdx | 2 +- docs/src/content/docs/overview.md | 37 +++++-- 5 files changed, 42 insertions(+), 133 deletions(-) delete mode 100644 docs/src/content/docs/commons/upgrade.md diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 012870d12..8c4a70a61 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -23,8 +23,7 @@ export default defineConfig({ label: 'Overview', items: [ { label: 'Spring Data Valkey', slug: 'overview' }, - { label: 'Upgrading Spring Data', slug: 'commons/upgrade' }, - { label: 'Migration Guides', slug: 'commons/migration' }, + { label: 'Migrating Spring Data', slug: 'commons/migration' }, ] }, { diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md index f3ad460aa..4f005a0b2 100644 --- a/docs/src/content/docs/commons/migration.md +++ b/docs/src/content/docs/commons/migration.md @@ -1,114 +1,15 @@ --- -title: Migration Guides -description: Migration steps, deprecations, and removals +title: Migrating Spring Data +description: Instructions for migrating from Spring Data Redis to Spring Data Valkey --- -This section contains details about migration steps, deprecations, and removals. - -## Upgrading from 2.x to 3.x - -### Re-/moved Types - -| Type | Replacement | -|------|-------------| -| o.s.d.valkey.Version | o.s.d.util.Version | -| o.s.d.valkey.VersionParser | - | -| o.s.d.valkey.connection.ValkeyZSetCommands.Aggregate | o.s.d.valkey.connection.zset.Aggregate | -| o.s.d.valkey.connection.ValkeyZSetCommands.Tuple | o.s.d.valkey.connection.zset.Tuple | -| o.s.d.valkey.connection.ValkeyZSetCommands.Weights | o.s.d.valkey.connection.zset.Weights | -| o.s.d.valkey.connection.ValkeyZSetCommands.Range | o.s.d.domain.Range | -| o.s.d.valkey.connection.ValkeyZSetCommands.Limit | o.s.d.valkey.connection.Limit.java | -| o.s.d.valkey.connection.jedis.JedisUtils | - | -| o.s.d.valkey.connection.jedis.JedisVersionUtil | - | -| o.s.d.valkey.core.convert.CustomConversions | o.s.d.convert.CustomConversions | - -### Changed Methods and Types - -*Table 1. Core* - -| Type | Method | Replacement | -|------|--------|-------------| -| o.s.d.valkey.core.Cursor | open | - | -| o.s.d.valkey.core.ValkeyTemplate | execute | doWithKeys | -| o.s.d.valkey.stream.StreamMessageListenerContainer | isAutoAck | isAutoAcknowledge | -| o.s.d.valkey.stream.StreamMessageListenerContainer | autoAck | autoAcknowledge | - -*Table 2. Valkey Connection* - -| Type | Method | Replacement | -|------|--------|-------------| -| o.s.d.valkey.connection.ClusterCommandExecutionFailureException | getCauses | getSuppressed | -| o.s.d.valkey.connection.ValkeyConnection | bgWriteAof | bgReWriteAof | -| o.s.d.valkey.connection.ValkeyConnection | slaveOf | replicaOf | -| o.s.d.valkey.connection.ValkeyConnection | slaveOfNoOne | replicaOfNoOne | -| o.s.d.valkey.connection.ReactiveClusterCommands | clusterGetSlaves | clusterGetReplicas | -| o.s.d.valkey.connection.ReactiveClusterCommands | clusterGetMasterSlaveMap | clusterGetMasterReplicaMap | -| o.s.d.valkey.connection.ReactiveKeyCommands | getNewName | getNewKey | -| o.s.d.valkey.connection.ValkeyClusterNode.Flag | SLAVE | REPLICA | -| o.s.d.valkey.connection.ValkeyClusterNode.Builder | slaveOf | replicaOf | -| o.s.d.valkey.connection.ValkeyNode | isSlave | isReplica | -| o.s.d.valkey.connection.ValkeySentinelCommands | slaves | replicas | -| o.s.d.valkey.connection.ValkeyServer | getNumberSlaves | getNumberReplicas | -| o.s.d.valkey.connection.ValkeyServerCommands | slaveOf | replicaOf | -| o.s.d.valkey.core.ClusterOperations | getSlaves | getReplicas | -| o.s.d.valkey.core.ValkeyOperations | slaveOf | replicaOf | - -*Table 3. Valkey Operations* - -| Type | Method | Replacement | -|------|--------|-------------| -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoAdd | add | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoDist | distance | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoHash | hash | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoPos | position | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRadius | radius | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRadiusByMember | radius | -| o.s.d.valkey.core.GeoOperations & BoundGeoOperations | geoRemove | remove | - -*Table 4. Valkey Cache* - -| Type | Method | Replacement | -|------|--------|-------------| -| o.s.d.valkey.cache.ValkeyCacheConfiguration | prefixKeysWith | prefixCacheNameWith | -| o.s.d.valkey.cache.ValkeyCacheConfiguration | getKeyPrefix | getKeyPrefixFor | - -### Jedis - -Please read the Jedis [upgrading guide](https://github.com/valkey/jedis/blob/v4.0.0/docs/3to4.md) which covers important driver changes. - -*Table 5. Jedis Valkey Connection* - -| Type | Method | Replacement | -|------|--------|-------------| -| o.s.d.valkey.connection.jedis.JedisConnectionFactory | getShardInfo | _can be obtained via JedisClientConfiguration_ | -| o.s.d.valkey.connection.jedis.JedisConnectionFactory | setShardInfo | _can be set via JedisClientConfiguration_ | -| o.s.d.valkey.connection.jedis.JedisConnectionFactory | createCluster | _now requires a `Connection` instead of `Jedis` instance_ | -| o.s.d.valkey.connection.jedis.JedisConverters | | has package visibility now | -| o.s.d.valkey.connection.jedis.JedisConverters | tuplesToTuples | - | -| o.s.d.valkey.connection.jedis.JedisConverters | stringListToByteList | - | -| o.s.d.valkey.connection.jedis.JedisConverters | stringSetToByteSet | - | -| o.s.d.valkey.connection.jedis.JedisConverters | stringMapToByteMap | - | -| o.s.d.valkey.connection.jedis.JedisConverters | tupleSetToTupleSet | - | -| o.s.d.valkey.connection.jedis.JedisConverters | toTupleSet | - | -| o.s.d.valkey.connection.jedis.JedisConverters | toDataAccessException | o.s.d.valkey.connection.jedis.JedisExceptionConverter#convert | - -#### Transactions / Pipelining - -Pipelining and Transactions are now mutually exclusive. -The usage of server or connection commands in pipeline/transactions mode is no longer possible. - -### Lettuce - -#### Lettuce Pool - -`LettucePool` and its implementation `DefaultLettucePool` have been removed without replacement. -Please refer to the [driver documentation](https://lettuce.io/core/release/reference/index.html#_connection_pooling) for driver native pooling capabilities. -Methods accepting pooling parameters have been updated. -This effects methods on `LettuceConnectionFactory` and `LettuceConnection`. - -#### Lettuce Authentication - -`AuthenticatingRedisClient` has been removed without replacement. -Please refer to the [driver documentation](https://lettuce.io/core/release/reference/index.html#basic.redisuri) for `RedisURI` to set authentication data. +Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive [Migration Guide](https://github.com/valkey-io/spring-data-valkey/blob/main/MIGRATION.md) in the Spring Data Valkey repository. +The migration guide covers: +- **Dependency Changes** - Updated Maven/Gradle dependencies for Spring Boot and vanilla Spring +- **Package Name Changes** - Spring Data packages: `org.springframework.data.redis.*` → `io.valkey.springframework.data.*`, Spring Boot packages: `org.springframework.boot.*.redis.*` → `io.valkey.springframework.boot.*.valkey.*` +- **Class Name Changes** - Redis classes renamed to Valkey equivalents (`RedisTemplate` → `ValkeyTemplate`, `@EnableRedisRepositories` → `@EnableValkeyRepositories`, etc.) +- **Configuration Changes** - Updated property names and configuration classes +- **Testing Changes** - Testcontainers and test configuration updates +- **Valkey GLIDE Integration** - New high-performance driver support alongside Lettuce and Jedis diff --git a/docs/src/content/docs/commons/upgrade.md b/docs/src/content/docs/commons/upgrade.md deleted file mode 100644 index 422ac9c1b..000000000 --- a/docs/src/content/docs/commons/upgrade.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Upgrading Spring Data -description: Instructions for upgrading Spring Data versions ---- - -Instructions for how to upgrade from earlier versions of Spring Data are provided on the project [wiki](https://github.com/spring-projects/spring-data-commons/wiki). -Follow the links in the [release notes section](https://github.com/spring-projects/spring-data-commons/wiki#release-notes) to find the version that you want to upgrade to. - -Upgrading instructions are always the first item in the release notes. If you are more than one release behind, please make sure that you also review the release notes of the versions that you jumped. - -Once you've decided to upgrade your application, you can find detailed information regarding specific features in the rest of the document. -You can find [migration guides](/commons/migration/) specific to major version migrations at the end of this document. - -Spring Data's documentation is specific to that version, so any information that you find in here will contain the most up-to-date changes that are in that version. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 00ee282f4..204e8c749 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -35,7 +35,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; [Learn more →](/observability/) - Upgrade from Spring Data Valkey + Migrate from Spring Data Redis [Learn more →](/commons/migration/) diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index 72a174c30..b1503799d 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -3,15 +3,38 @@ title: Spring Data Valkey description: Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. --- - _Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. It eases development of applications with a consistent programming model that need to access Valkey data sources._ -| Section | Description | -|---------|-------------| -| [Valkey](../valkey) | Valkey support and connectivity | -| [Valkey Repositories](../repositories) | Valkey Repositories | -| [Observability](../observability) | Observability Integration | -| [Wiki](https://github.com/spring-projects/spring-data-commons/wiki) | What's New, Upgrade Notes, Supported Versions, additional cross-version information. | + ++++ + + + + + + + + + + + + + + + + + + + + + + + + +
SectionDescription
ValkeyValkey support and connectivity
Valkey RepositoriesValkey Repositories
ObservabilityObservability Integration
WikiWhat's New, Upgrade Notes, Supported Versions, additional cross-version information.
## Authors From 868343e6b21440ba2ae33b6f0ace556e25bb017c Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 28 Jan 2026 09:57:49 -0800 Subject: [PATCH 05/21] Update external navbar links Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 8 +------- docs/src/content/docs/appendix.md | 2 +- docs/src/content/docs/commons/migration.md | 2 +- docs/src/content/docs/index.mdx | 2 ++ docs/src/content/docs/overview.md | 10 ++++++++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8c4a70a61..7293172ef 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -73,13 +73,7 @@ export default defineConfig({ }, { label: 'Observability', slug: 'observability' }, { label: 'Appendix', slug: 'appendix' }, - { - label: 'External Links', - items: [ - { label: 'Javadoc', link: '/api/java/index.html' }, - { label: 'Wiki', link: 'https://github.com/spring-projects/spring-data-commons/wiki' }, - ] - }, + { label: 'Javadoc ↗', link: '/api/java/index.html', attrs: { target: '_blank' } }, ], }), ], diff --git a/docs/src/content/docs/appendix.md b/docs/src/content/docs/appendix.md index 1b3911268..452a947d0 100644 --- a/docs/src/content/docs/appendix.md +++ b/docs/src/content/docs/appendix.md @@ -5,7 +5,7 @@ description: Additional reference information ## Schema -[Spring Data Valkey Schema (valkey-namespace)](https://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd) +[Spring Data Valkey Schema (valkey-namespace)](https://spring.valkey.io/schema/valkey/spring-valkey-1.0.xsd) ## Supported Commands diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md index 4f005a0b2..babe929b4 100644 --- a/docs/src/content/docs/commons/migration.md +++ b/docs/src/content/docs/commons/migration.md @@ -3,7 +3,7 @@ title: Migrating Spring Data description: Instructions for migrating from Spring Data Redis to Spring Data Valkey --- -Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive [Migration Guide](https://github.com/valkey-io/spring-data-valkey/blob/main/MIGRATION.md) in the Spring Data Valkey repository. +Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive Migration Guide in the Spring Data Valkey repository. The migration guide covers: diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 204e8c749..4f48b2129 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -12,6 +12,8 @@ hero: - text: View on GitHub link: https://github.com/valkey-io/spring-data-valkey icon: external + attrs: + target: _blank --- import { Card, CardGrid } from '@astrojs/starlight/components'; diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index b1503799d..5a3837399 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -30,18 +30,24 @@ _Spring Data Valkey provides Valkey connectivity and repository support for the Observability Integration -Wiki -What's New, Upgrade Notes, Supported Versions, additional cross-version information. +Valkey Project ↗ +Official Valkey project site and community resources ## Authors +*Original Spring Data Redis Authors:* Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch, Jay Bryant +*Spring Data Valkey Contributors:* +Jeremy Parr-Pearson + ## Copyright (C) 2008-2024 VMware, Inc. +(C) 2025-2026 Valkey Contributors + Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. From 3e8ab63214ac19922bc64a0359fe64a66cce1fe4 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 28 Jan 2026 11:20:38 -0800 Subject: [PATCH 06/21] Update valkey pages Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 9 +- docs/src/content/docs/commons/migration.md | 2 +- docs/src/content/docs/observability.md | 4 + docs/src/content/docs/valkey.md | 7 +- docs/src/content/docs/valkey/cluster.md | 2 +- .../content/docs/valkey/connection-modes.md | 34 +++++-- docs/src/content/docs/valkey/drivers.md | 95 ++++++++++++++++--- .../content/docs/valkey/getting-started.md | 51 ++++++++++ .../content/docs/valkey/getting-started.mdx | 86 ----------------- docs/src/content/docs/valkey/template.mdx | 16 ++-- docs/src/content/docs/valkey/transactions.md | 2 +- docs/src/content/docs/valkey/valkey-cache.md | 1 + 12 files changed, 185 insertions(+), 124 deletions(-) create mode 100644 docs/src/content/docs/valkey/getting-started.md delete mode 100644 docs/src/content/docs/valkey/getting-started.mdx diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 7293172ef..75f0a2d25 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -1,10 +1,16 @@ // @ts-check import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import rehypeExternalLinks from 'rehype-external-links'; // https://astro.build/config export default defineConfig({ site: 'https://spring.valkey.io', + markdown: { + rehypePlugins: [ + [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }] + ] + }, integrations: [ starlight({ title: 'Spring Data Valkey', @@ -73,7 +79,8 @@ export default defineConfig({ }, { label: 'Observability', slug: 'observability' }, { label: 'Appendix', slug: 'appendix' }, - { label: 'Javadoc ↗', link: '/api/java/index.html', attrs: { target: '_blank' } }, + { label: 'Valkey Project ↗', link: 'https://valkey.io/', attrs: { target: '_blank' } }, + { label: 'Javadoc ↗', link: 'https://spring.valkey.io/api/java/index.html', attrs: { target: '_blank' } }, ], }), ], diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md index babe929b4..4f005a0b2 100644 --- a/docs/src/content/docs/commons/migration.md +++ b/docs/src/content/docs/commons/migration.md @@ -3,7 +3,7 @@ title: Migrating Spring Data description: Instructions for migrating from Spring Data Redis to Spring Data Valkey --- -Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive Migration Guide in the Spring Data Valkey repository. +Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive [Migration Guide](https://github.com/valkey-io/spring-data-valkey/blob/main/MIGRATION.md) in the Spring Data Valkey repository. The migration guide covers: diff --git a/docs/src/content/docs/observability.md b/docs/src/content/docs/observability.md index 682ba0147..ea0115d65 100644 --- a/docs/src/content/docs/observability.md +++ b/docs/src/content/docs/observability.md @@ -7,6 +7,10 @@ Getting insights from an application component about its operations, timing and Spring Data Valkey ships with a Micrometer integration through the Lettuce driver to collect observations during Valkey interaction. Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Valkey command. +:::note[Driver Support] +Observability integration is currently only supported with the Lettuce driver. Valkey GLIDE and Jedis do not support Spring observability at this time. +::: + To enable the integration, apply the following configuration to `LettuceClientConfiguration`: ```java diff --git a/docs/src/content/docs/valkey.md b/docs/src/content/docs/valkey.md index dbf470727..9954e6bbf 100644 --- a/docs/src/content/docs/valkey.md +++ b/docs/src/content/docs/valkey.md @@ -4,12 +4,9 @@ description: Spring Data Valkey support and connectivity --- One of the key-value stores supported by Spring Data is [Valkey](https://valkey.io). -To quote the Valkey project home page: +To quote the Valkey project documentation: -> Valkey is an advanced key-value store. -> It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. -> All this data types can be manipulated with atomic operations to push/pop elements, add/remove elements, perform server side union, intersection, difference between sets, and so forth. -> Valkey supports different kind of sorting abilities. +> "Valkey is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. Valkey provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams." Spring Data Valkey provides easy configuration and access to Valkey from Spring applications. It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns. diff --git a/docs/src/content/docs/valkey/cluster.md b/docs/src/content/docs/valkey/cluster.md index 757b011b6..22d3e2f89 100644 --- a/docs/src/content/docs/valkey/cluster.md +++ b/docs/src/content/docs/valkey/cluster.md @@ -140,5 +140,5 @@ clusterOps.shutdown(NODE_7379); // ``` :::note -Valkey Cluster pipelining is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Valkey Cluster pipelining is currently only supported through the Lettuce driver. Valkey GLIDE and Jedis do not support pipelining in cluster mode. The Lettuce driver has exceptions for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. ::: diff --git a/docs/src/content/docs/valkey/connection-modes.md b/docs/src/content/docs/valkey/connection-modes.md index dda57a8c5..4d631a344 100644 --- a/docs/src/content/docs/valkey/connection-modes.md +++ b/docs/src/content/docs/valkey/connection-modes.md @@ -10,12 +10,20 @@ Each mode of operation requires specific configuration that is explained in the The easiest way to get started is by using Valkey Standalone with a single Valkey server, -Configure `io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory` or `io.valkey.springframework.data.connection.jedis.JedisConnectionFactory`, as shown in the following example: +Configure `io.valkey.springframework.data.connection.valkeyglide.ValkeyGlideConnectionFactory`, `io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory` or `io.valkey.springframework.data.connection.jedis.JedisConnectionFactory`, as shown in the following example: ```java @Configuration class ValkeyStandaloneConfiguration { + /** + * Valkey GLIDE + */ + @Bean + public ValkeyConnectionFactory valkeyGlideConnectionFactory() { + return new ValkeyGlideConnectionFactory(new ValkeyStandaloneConfiguration("server", 6379)); + } + /** * Lettuce */ @@ -37,23 +45,23 @@ class ValkeyStandaloneConfiguration { ## Write to Master, Read from Replica The Valkey Master/Replica setup -- without automatic failover (for automatic failover see: [Sentinel](/valkey/connection-modes#valkey-sentinel)) -- not only allows data to be safely stored at more nodes. -It also allows, by using [Lettuce](/valkey/drivers#configuring-the-lettuce-connector), reading data from replicas while pushing writes to the master. -You can set the read/write strategy to be used by using `LettuceClientConfiguration`, as shown in the following example: +It also allows, by using [Valkey GLIDE](/valkey/drivers#configuring-the-valkey-glide-connector), reading data from replicas while pushing writes to the master. +You can set the read/write strategy to be used by using `ValkeyGlideClientConfiguration`, as shown in the following example: ```java @Configuration class WriteToMasterReadFromReplicaConfiguration { @Bean - public LettuceConnectionFactory valkeyConnectionFactory() { + public ValkeyGlideConnectionFactory valkeyConnectionFactory() { - LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() - .readFrom(REPLICA_PREFERRED) + ValkeyGlideClientConfiguration clientConfig = ValkeyGlideClientConfiguration.builder() + .readFrom(glide.api.models.configuration.ReadFrom.PREFER_REPLICA) .build(); ValkeyStandaloneConfiguration serverConfig = new ValkeyStandaloneConfiguration("server", 6379); - return new LettuceConnectionFactory(serverConfig, clientConfig); + return new ValkeyGlideConnectionFactory(serverConfig, clientConfig); } } ``` @@ -66,6 +74,10 @@ For environments reporting non-public addresses through the `INFO` command (for For dealing with high-availability Valkey, Spring Data Valkey has support for [Valkey Sentinel](https://valkey.io/topics/sentinel), using `io.valkey.springframework.data.connection.ValkeySentinelConfiguration`, as shown in the following example: +:::note[Driver Support] +Valkey Sentinel is currently supported by Lettuce and Jedis drivers. Valkey GLIDE support for Sentinel is planned for a future release. +::: + ```java /** * Lettuce @@ -149,7 +161,13 @@ public class AppConfig { */ @Autowired ClusterConfigurationProperties clusterProperties; - public @Bean ValkeyConnectionFactory connectionFactory() { + public @Bean ValkeyConnectionFactory valkeyGlideConnectionFactory() { + + return new ValkeyGlideConnectionFactory( + new ValkeyClusterConfiguration(clusterProperties.getNodes())); + } + + public @Bean ValkeyConnectionFactory lettuceConnectionFactory() { return new LettuceConnectionFactory( new ValkeyClusterConfiguration(clusterProperties.getNodes())); diff --git a/docs/src/content/docs/valkey/drivers.md b/docs/src/content/docs/valkey/drivers.md index 05cdd8e74..c7dafaa61 100644 --- a/docs/src/content/docs/valkey/drivers.md +++ b/docs/src/content/docs/valkey/drivers.md @@ -42,21 +42,86 @@ The following overview explains features that are supported by the individual Va *Table 1. Feature Availability across Valkey Connectors* -| Supported Feature | Lettuce | Jedis | -|-------------------|---------|-------| -| Standalone Connections | X | X | -| [Master/Replica Connections](/valkey/connection-modes#write-to-master-read-from-replica) | X | X | -| [Valkey Sentinel](/valkey/connection-modes#valkey-sentinel) | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | -| [Valkey Cluster](/valkey/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | -| Transport Channels | TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets | TCP | -| Connection Pooling | X (using `commons-pool2`) | X (using `commons-pool2`) | -| Other Connection Features | Singleton-connection sharing for non-blocking commands | Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. | -| SSL Support | X | X | -| [Pub/Sub](/valkey/pubsub) | X | X | -| [Pipelining](/valkey/pipelining) | X | X (Pipelining and Transactions mutually exclusive) | -| [Transactions](/valkey/transactions) | X | X (Pipelining and Transactions mutually exclusive) | -| Datatype support | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | -| Reactive (non-blocking) API | X | | +| Supported Feature | Valkey GLIDE | Lettuce | Jedis | +|-------------------|--------------|---------|-------| +| Standalone Connections | X | X | X | +| [Master/Replica Connections](/valkey/connection-modes#write-to-master-read-from-replica) | X | X | X | +| [Valkey Sentinel](/valkey/connection-modes#valkey-sentinel) | | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | +| [Valkey Cluster](/valkey/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | +| Transport Channels | TCP | TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets | TCP | +| Connection Pooling | X (using `LinkedBlockingQueue`) | X (using `commons-pool2`) | X (using `commons-pool2`) | +| Other Connection Features | High-performance async operations | Singleton-connection sharing for non-blocking commands | Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. | +| SSL Support | X | X | X | +| [Pub/Sub](/valkey/pubsub) | X | X | X | +| [Pipelining](/valkey/pipelining) | X | X | X (Pipelining and Transactions mutually exclusive) | +| [Transactions](/valkey/transactions) | X | X | X (Pipelining and Transactions mutually exclusive) | +| Datatype support | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog | +| Reactive (non-blocking) API | | X | | + +## Configuring the Valkey GLIDE Connector + +[Valkey GLIDE](https://github.com/valkey-io/valkey-glide) is a high-performance, cross-language client library for Valkey, supported by Spring Data Valkey through the `io.valkey.springframework.data.connection.valkeyglide` package. + +*Add the following to the pom.xml files `dependencies` element:* + +```xml + + + + + + io.valkey + valkey-glide + 2.3.0 + linux-x86_64 + + + +``` + +The following example shows how to create a new Valkey GLIDE connection factory: + +```java +@Configuration +class AppConfig { + + @Bean + public ValkeyGlideConnectionFactory valkeyConnectionFactory() { + + return new ValkeyGlideConnectionFactory(new ValkeyStandaloneConfiguration("server", 6379)); + } +} +``` + +Valkey GLIDE provides built-in connection pooling and high-performance async operations. +You can configure various client options including SSL, timeouts, and OpenTelemetry observability. + +The following example shows a more sophisticated configuration with SSL, timeouts, and observability: + +```java +@Bean +public ValkeyGlideConnectionFactory valkeyGlideConnectionFactory() { + + var otelConfig = new ValkeyGlideClientConfiguration.OpenTelemetryForGlide( + "http://localhost:4318/v1/traces", + "http://localhost:4318/v1/metrics", + 10, + 1000L + ); + + ValkeyGlideClientConfiguration clientConfig = ValkeyGlideClientConfiguration.builder() + .useSsl() + .commandTimeout(Duration.ofSeconds(2)) + .connectionTimeout(Duration.ofSeconds(1)) + .useOpenTelemetry(otelConfig) + .maxPoolSize(16) + .build(); + + return new ValkeyGlideConnectionFactory(new ValkeyStandaloneConfiguration("localhost", 6379), clientConfig); +} +``` + +For more detailed client configuration options, see `io.valkey.springframework.data.connection.valkeyglide.ValkeyGlideClientConfiguration`. ## Configuring the Lettuce Connector diff --git a/docs/src/content/docs/valkey/getting-started.md b/docs/src/content/docs/valkey/getting-started.md new file mode 100644 index 000000000..258f3ca1a --- /dev/null +++ b/docs/src/content/docs/valkey/getting-started.md @@ -0,0 +1,51 @@ +--- +title: Getting Started +description: Getting Started documentation +--- + +An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-valkey) or create a Spring project in [Spring Tools](https://spring.io/tools). +## Examples Repository + +The GitHub [spring-data-valkey examples](https://github.com/valkey-io/spring-data-valkey/tree/main/examples) repository hosts several examples that you can download and play around with to get a feel for how the library works. +## Hello World + +First, you need to set up a running Valkey server. +Spring Data Valkey requires Valkey 2.6 or above and Spring Data Valkey integrates with [Valkey GLIDE](https://github.com/valkey-io/valkey-glide), [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/valkey/jedis), popular open-source Java libraries for Valkey. + +Now you can create a simple Java application that stores and reads a value to and from Valkey. + +Create the main application to run, as the following example shows: + +```java +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import io.valkey.springframework.data.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.connection.glide.ValkeyGlideConnectionFactory; +import io.valkey.springframework.data.core.ValkeyTemplate; + +public class ValkeyApplication { + + private static final Log LOG = LogFactory.getLog(ValkeyApplication.class); + + public static void main(String[] args) { + + ValkeyConnectionFactory factory = new ValkeyGlideConnectionFactory(); + + ValkeyTemplate template = new ValkeyTemplate<>(); + template.setConnectionFactory(factory); + template.afterPropertiesSet(); + + template.opsForValue().set("foo", "bar"); + + LOG.info("Value at foo:" + template.opsForValue().get("foo")); + + template.getConnectionFactory().getConnection().close(); + } +} +``` +Even in this simple example, there are a few notable things to point out: + +* You can create an instance of `io.valkey.springframework.data.core.ValkeyTemplate` with a `io.valkey.springframework.data.connection.ValkeyConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. +* For reactive programming with `ReactiveValkeyTemplate`, only Lettuce is supported. +* There's no single way to use Valkey as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/valkey/getting-started.mdx b/docs/src/content/docs/valkey/getting-started.mdx deleted file mode 100644 index 977671a14..000000000 --- a/docs/src/content/docs/valkey/getting-started.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Getting Started -description: Getting Started documentation ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-valkey) or create a Spring project in [Spring Tools](https://spring.io/tools). -## Examples Repository - -The GitHub [spring-data-examples repository](https://github.com/spring-projects/spring-data-examples) hosts several examples that you can download and play around with to get a feel for how the library works. -## Hello World - -First, you need to set up a running Valkey server. -Spring Data Valkey requires Valkey 2.6 or above and Spring Data Valkey integrates with [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/valkey/jedis), two popular open-source Java libraries for Valkey. - -Now you can create a simple Java application that stores and reads a value to and from Valkey. - -Create the main application to run, as the following example shows: - - - - -```java -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import io.valkey.springframework.data.connection.ValkeyConnectionFactory; -import io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory; -import io.valkey.springframework.data.core.ValkeyTemplate; - -public class ValkeyApplication { - - private static final Log LOG = LogFactory.getLog(ValkeyApplication.class); - - public static void main(String[] args) { - - ValkeyConnectionFactory factory = new LettuceConnectionFactory(); - - ValkeyTemplate template = new ValkeyTemplate<>(); - template.setConnectionFactory(factory); - template.afterPropertiesSet(); - - template.opsForValue().set("foo", "bar"); - - LOG.info("Value at foo:" + template.opsForValue().get("foo")); - - template.getConnectionFactory().getConnection().close(); - } -} -``` - - - - -```java -import reactor.core.publisher.Mono; - -import io.valkey.springframework.data.connection.ReactiveValkeyConnectionFactory; -import io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory; -import io.valkey.springframework.data.core.ReactiveValkeyTemplate; -import io.valkey.springframework.data.serialization.ValkeySerializationContext; - -public class ReactiveValkeyApplication { - - public static void main(String[] args) { - - ReactiveValkeyConnectionFactory factory = new LettuceConnectionFactory(); - - ReactiveValkeyTemplate template = new ReactiveValkeyTemplate<>(factory, ValkeySerializationContext.string()); - - template.opsForValue().set("foo", "bar") - .then(template.opsForValue().get("foo")) - .doOnNext(System.out::println) - .then(Mono.fromRunnable(() -> factory.getReactiveConnection().close())) - .subscribe(); - } -} -``` - - - -Even in this simple example, there are a few notable things to point out: - -* You can create an instance of `io.valkey.springframework.data.core.ValkeyTemplate` (or `io.valkey.springframework.data.core.ReactiveValkeyTemplate`for reactive usage) with a `io.valkey.springframework.data.connection.ValkeyConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. -* There's no single way to use Valkey as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/valkey/template.mdx b/docs/src/content/docs/valkey/template.mdx index ac0f9ce3e..8d5748188 100644 --- a/docs/src/content/docs/valkey/template.mdx +++ b/docs/src/content/docs/valkey/template.mdx @@ -17,6 +17,10 @@ The preferred way to reference operations on a `[Reactive]ValkeyTemplate` instan `[Reactive]ValkeyOperations` interface. ::: +:::note[Driver Support] +Reactive programming is currently only supported by the Lettuce driver. Valkey GLIDE and Jedis do not support reactive operations. For imperative (blocking) operations, all drivers are supported. +::: + Moreover, the template provides operations views (following the grouping from the Valkey command [reference](https://valkey.io/commands)) that offer rich, generified interfaces for working against a certain type or certain key (through the `KeyBound` interfaces) as described in the following table:
@@ -88,8 +92,8 @@ The container automatically performs the conversion, eliminating the `opsFor[X]` class MyConfig { @Bean - LettuceConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); + ValkeyGlideConnectionFactory connectionFactory() { + return new ValkeyGlideConnectionFactory(); } @Bean @@ -131,7 +135,7 @@ class MyConfig { xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + ... @@ -197,8 +201,8 @@ The following listings show an example: class ValkeyConfiguration { @Bean - LettuceConnectionFactory valkeyConnectionFactory() { - return new LettuceConnectionFactory(); + ValkeyGlideConnectionFactory valkeyConnectionFactory() { + return new ValkeyGlideConnectionFactory(); } @Bean @@ -240,7 +244,7 @@ class ValkeyConfiguration { xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/docs/src/content/docs/valkey/transactions.md b/docs/src/content/docs/valkey/transactions.md index 4b3d0e51f..02101ab37 100644 --- a/docs/src/content/docs/valkey/transactions.md +++ b/docs/src/content/docs/valkey/transactions.md @@ -79,7 +79,7 @@ public class ValkeyTxContextConfiguration { @Bean public ValkeyConnectionFactory valkeyConnectionFactory() { - // jedis || Lettuce + // jedis || Lettuce || GLIDE } @Bean diff --git a/docs/src/content/docs/valkey/valkey-cache.md b/docs/src/content/docs/valkey/valkey-cache.md index cd08281ca..c76a5e6b1 100644 --- a/docs/src/content/docs/valkey/valkey-cache.md +++ b/docs/src/content/docs/valkey/valkey-cache.md @@ -86,6 +86,7 @@ The `KEYS` batch strategy is fully supported using any driver and Valkey operati `SCAN` is fully supported when using the Lettuce driver. Jedis supports `SCAN` only in non-clustered modes. +Valkey GLIDE supports `SCAN` in standalone mode, with cluster mode support planned for a future release. The following table lists the default settings for `ValkeyCacheManager`: From 70594f7864d8e11233de6d5ffea9cd3ff7e42edb Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 29 Jan 2026 10:56:07 -0800 Subject: [PATCH 07/21] Update repositories pages Signed-off-by: Jeremy Parr-Pearson --- docs/src/content/docs/observability.md | 71 +++++++++++++++++-- docs/src/content/docs/preface.md | 2 +- docs/src/content/docs/valkey/drivers.md | 2 +- .../content/docs/valkey/getting-started.md | 4 +- .../valkey-repositories/cdi-integration.md | 2 +- .../docs/valkey/valkey-repositories/usage.md | 2 +- 6 files changed, 73 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/observability.md b/docs/src/content/docs/observability.md index ea0115d65..3ec4dc850 100644 --- a/docs/src/content/docs/observability.md +++ b/docs/src/content/docs/observability.md @@ -4,13 +4,76 @@ description: Observability Integration for monitoring and metrics --- Getting insights from an application component about its operations, timing and relation to application code is crucial to understand latency. -Spring Data Valkey ships with a Micrometer integration through the Lettuce driver to collect observations during Valkey interaction. -Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Valkey command. -:::note[Driver Support] -Observability integration is currently only supported with the Lettuce driver. Valkey GLIDE and Jedis do not support Spring observability at this time. +Spring Data Valkey provides observability integration through two approaches: +- *OpenTelemetry integration* through Valkey GLIDE for direct OTLP export +- *Micrometer integration* through the Lettuce driver for Spring-based metrics and tracing + + +## OpenTelemetry Integration (GLIDE) + +Valkey GLIDE provides built-in OpenTelemetry support for direct export of traces and metrics to OTLP endpoints. +This integration operates independently of Spring's observability infrastructure and exports telemetry data directly to OpenTelemetry collectors. + +### Spring Boot Configuration + +When using Spring Boot, OpenTelemetry can be enabled via application properties: + +```properties +# Enable OpenTelemetry instrumentation in GLIDE +spring.data.valkey.valkey-glide.open-telemetry.enabled=true + +# Configure trace and metric endpoints (defaults shown) +spring.data.valkey.valkey-glide.open-telemetry.traces-endpoint=http://localhost:4318/v1/traces +spring.data.valkey.valkey-glide.open-telemetry.metrics-endpoint=http://localhost:4318/v1/metrics + +# Optionally configure sampling and flush behavior +spring.data.valkey.valkey-glide.open-telemetry.sample-percentage=1 +spring.data.valkey.valkey-glide.open-telemetry.flush-interval-ms=5000 +``` + +### Programmatic Configuration + +For non-Spring Boot applications, configure OpenTelemetry programmatically: + +```java +@Configuration +public class ValkeyGlideOpenTelemetryConfig { + + @Bean + public ValkeyGlideConnectionFactory valkeyConnectionFactory() { + + ValkeyGlideClientConfiguration clientConfig = ValkeyGlideClientConfiguration.builder() + .useOpenTelemetry(ValkeyGlideClientConfiguration.OpenTelemetryForGlide.defaults()) + .build(); + + return new ValkeyGlideConnectionFactory(new ValkeyStandaloneConfiguration(), clientConfig); + } +} +``` + +For custom OpenTelemetry configuration: + +```java +ValkeyGlideClientConfiguration clientConfig = ValkeyGlideClientConfiguration.builder() + .useOpenTelemetry(new ValkeyGlideClientConfiguration.OpenTelemetryForGlide( + "http://jaeger:4318/v1/traces", // traces endpoint + "http://prometheus:4318/v1/metrics", // metrics endpoint + 10, // sample percentage + 1000L // flush interval (ms) + )) + .build(); +``` + +:::note +GLIDE's OpenTelemetry integration exports telemetry data directly to OTLP endpoints and does not integrate with Spring's Micrometer-based observability infrastructure. Automatically collected telemetry includes command duration, success/failure rates, connection pool metrics, and distributed traces. ::: +## Micrometer Integration (Lettuce) + +Spring Data Valkey ships with a Micrometer integration through the Lettuce driver to collect observations during Valkey interaction. +Once the integration is set up, Micrometer will create meters and spans (for distributed tracing) for each Valkey command. + To enable the integration, apply the following configuration to `LettuceClientConfiguration`: ```java diff --git a/docs/src/content/docs/preface.md b/docs/src/content/docs/preface.md index ebf2d646c..cc106b1a7 100644 --- a/docs/src/content/docs/preface.md +++ b/docs/src/content/docs/preface.md @@ -41,7 +41,7 @@ It usually does not take more then five to ten minutes to go through them and, i ### Trying out the Samples -One can find various samples for key-value stores in the dedicated Spring Data example repo, at [https://github.com/spring-projects/spring-data-examples/tree/main/valkey](https://github.com/spring-projects/spring-data-examples/tree/main/valkey). +One can find various samples for key-value stores in the Spring Data Valkey repo, at [https://github.com/valkey-io/spring-data-valkey/tree/main/examples](https://github.com/valkey-io/spring-data-valkey/tree/main/examples). ## Requirements diff --git a/docs/src/content/docs/valkey/drivers.md b/docs/src/content/docs/valkey/drivers.md index c7dafaa61..03200a69e 100644 --- a/docs/src/content/docs/valkey/drivers.md +++ b/docs/src/content/docs/valkey/drivers.md @@ -201,7 +201,7 @@ Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for ## Configuring the Jedis Connector -[Jedis](https://github.com/valkey/jedis) is a community-driven connector supported by the Spring Data Valkey module through the `io.valkey.springframework.data.connection.jedis` package. +[Jedis](https://github.com/redis/jedis) is a community-driven connector supported by the Spring Data Valkey module through the `io.valkey.springframework.data.connection.jedis` package. *Add the following to the pom.xml files `dependencies` element:* diff --git a/docs/src/content/docs/valkey/getting-started.md b/docs/src/content/docs/valkey/getting-started.md index 258f3ca1a..b4961716f 100644 --- a/docs/src/content/docs/valkey/getting-started.md +++ b/docs/src/content/docs/valkey/getting-started.md @@ -6,11 +6,11 @@ description: Getting Started documentation An easy way to bootstrap setting up a working environment is to create a Spring-based project via [start.spring.io](https://start.spring.io/#!type=maven-project&dependencies=data-valkey) or create a Spring project in [Spring Tools](https://spring.io/tools). ## Examples Repository -The GitHub [spring-data-valkey examples](https://github.com/valkey-io/spring-data-valkey/tree/main/examples) repository hosts several examples that you can download and play around with to get a feel for how the library works. +The GitHub [spring-data-valkey](https://github.com/valkey-io/spring-data-valkey/tree/main/examples) repository hosts several examples that you can download and play around with to get a feel for how the library works. ## Hello World First, you need to set up a running Valkey server. -Spring Data Valkey requires Valkey 2.6 or above and Spring Data Valkey integrates with [Valkey GLIDE](https://github.com/valkey-io/valkey-glide), [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/valkey/jedis), popular open-source Java libraries for Valkey. +Spring Data Valkey requires Valkey 2.6 or above and Spring Data Valkey integrates with [Valkey GLIDE](https://github.com/valkey-io/valkey-glide), [Lettuce](https://github.com/lettuce-io/lettuce-core) and [Jedis](https://github.com/redis/jedis), popular open-source Java libraries for Valkey. Now you can create a simple Java application that stores and reads a value to and from Valkey. diff --git a/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md index e392b065c..db2fed7fe 100644 --- a/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md +++ b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md @@ -15,7 +15,7 @@ class ValkeyOperationsProducer { @Produces ValkeyConnectionFactory valkeyConnectionFactory() { - LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new ValkeyStandaloneConfiguration()); + ValkeyGlideConnectionFactory connectionFactory = new ValkeyGlideConnectionFactory(new ValkeyStandaloneConfiguration()); connectionFactory.afterPropertiesSet(); connectionFactory.start(); diff --git a/docs/src/content/docs/valkey/valkey-repositories/usage.md b/docs/src/content/docs/valkey/valkey-repositories/usage.md index 0c717f764..6376a275c 100644 --- a/docs/src/content/docs/valkey/valkey-repositories/usage.md +++ b/docs/src/content/docs/valkey/valkey-repositories/usage.md @@ -49,7 +49,7 @@ public class ApplicationConfig { @Bean public ValkeyConnectionFactory connectionFactory() { - return new LettuceConnectionFactory(); + return new ValkeyGlideConnectionFactory(); } @Bean From e297fa88ec9243f47476ea29dc19590621d9e3a9 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 29 Jan 2026 12:08:44 -0800 Subject: [PATCH 08/21] Add GitHub action to publish docs Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/docs.yml | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..2d4d2e72c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,79 @@ +name: Docs + +on: + workflow_dispatch: + # Uncomment to automatically push doc changes on merge to main + # push: + # branches: [main] + # paths: + # - 'docs/**' + # - 'spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs/package-lock.json + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + run: npm ci + working-directory: ./docs + + - name: Generate Javadocs + run: | + ./mvnw javadoc:javadoc -pl spring-data-valkey -DskipTests + mkdir -p docs/dist/api/java + cp -r spring-data-valkey/target/site/apidocs/* docs/dist/api/java/ + + - name: Copy Schema + run: | + mkdir -p docs/dist/schema/valkey + cp spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd docs/dist/schema/valkey/ + + - name: Build docs + run: npm run build + working-directory: ./docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 5017341758e7313cdb0e33a016f5eb26d96add02 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 29 Jan 2026 13:04:17 -0800 Subject: [PATCH 09/21] Update navbar names Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 2 +- docs/src/content/docs/index.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 75f0a2d25..d4d75a5d6 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -52,7 +52,7 @@ export default defineConfig({ ] }, { - label: 'Repositories', + label: 'Valkey Repositories', items: [ { label: 'Valkey Repositories Overview', slug: 'repositories' }, { label: 'Core concepts', slug: 'repositories/core-concepts' }, diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 4f48b2129..bfbc1411b 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -26,7 +26,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; [Learn more →](/valkey/) - + Repository abstraction for Valkey data access [Learn more →](/repositories/) From 4a3bdd829214d0b300a6a698dc038c4abded4308 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 30 Jan 2026 09:21:34 -0800 Subject: [PATCH 10/21] Add doc notes to devloper guide Signed-off-by: Jeremy Parr-Pearson --- DEVELOPER.md | 39 +++++++++++++++++++++++++++++++++++++++ docs/astro.config.mjs | 6 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index d82f12719..58b13385b 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -156,6 +156,45 @@ For non-Spring Boot applications, configure logging via `logback.xml` in your cl ``` +## Redis Source Alignment + +This repository is forked from Spring Data Redis and related projects. This section documents the source repositories to help with future upgrades, patches, and alignment. + +| Local Module | Redis Source Repository | Branch/Tag | Notes | +|--------------|------------------------|------------|-------| +| `./` | [spring-data-build](https://github.com/spring-projects/spring-data-build) | `3.5.1` | Parent for Spring Data modules | +| `spring-data-valkey/` | [spring-data-redis](https://github.com/spring-projects/spring-data-redis) | `3.5.1` | Core Spring Data Redis functionality | +| `spring-boot-starter-data-valkey/` | [spring-boot](https://github.com/spring-projects/spring-boot) | `3.5.1` | Spring Boot auto-configuration for Redis | +| `docs/` | [spring-data-redis](https://github.com/spring-projects/spring-data-redis) | `3.5.1` | Documentation | +| `examples/` | [spring-data-examples](https://github.com/spring-projects/spring-data-examples) | `main` | Redis examples | + +Key changes from Redis source: +- **Package names**: + - Spring Data: `org.springframework.data.redis.*` → `io.valkey.springframework.data.valkey.*` + - Spring Boot: `org.springframework.boot.*.redis.*` → `io.valkey.springframework.boot.*.valkey.*` +- **Class names and properties**: `*Redis*` → `*Valkey*` or `*redis*` → `*valkey*` +- **Dependencies**: + - Spring Data: `org.springframework.data:spring-data-redis` → `io.valkey.springframework.data:spring-data-valkey` + - Spring Boot: `org.springframework.boot:spring-boot-starter-data-redis` → `io.valkey.springframework.boot:spring-boot-starter-data-valkey` +- **Driver support**: Added Valkey GLIDE as the primary driver +- **Documentation**: Migrated from Antora/AsciiDoc to Starlight/Markdown + +## Deploying Documentation + +The documentation is deployed to GitHub Pages using GitHub Actions. This includes the Astro Starlight documentation under `docs/`, as well as the Spring Data Valkey JavaDocs and schema. + +Manually trigger the *Docs* workflow in GitHub Actions for a given branch to deploy the documentation on that branch. The documentation will then be available at `https://valkey-io.github.io/spring-data-valkey/` or `https://spring.valkey.io` (CNAME redirect). + +### Local Development + +```bash +$ cd docs +$ npm install +$ npm run dev +``` + +The documentation will be available at `http://localhost:4321/`. + ## Release Process ### Version Management diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index d4d75a5d6..eb15b2798 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -5,7 +5,11 @@ import rehypeExternalLinks from 'rehype-external-links'; // https://astro.build/config export default defineConfig({ - site: 'https://spring.valkey.io', + // TODO: Once CNAME record is added, change site and remove base property, + // and rename docs/public/CNAME.example to docs/public/CNAME + //site: 'https://spring.valkey.io', + site: 'https://valkey-io.github.io', + base: process.env.NODE_ENV === 'production' ? '/spring-data-valkey' : '/', markdown: { rehypePlugins: [ [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }] From a64775d818c54f2e507aaf2405af4a624300a8c6 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 30 Jan 2026 10:06:21 -0800 Subject: [PATCH 11/21] Update doc links Signed-off-by: Jeremy Parr-Pearson --- DEVELOPER.md | 4 ++-- MIGRATION.md | 8 ++++---- README.md | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 58b13385b..58307f0c1 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -234,6 +234,6 @@ $ git push origin v1.0.0 ## Additional Resources * [Valkey Documentation](https://valkey.io/docs/) -* [Spring Data Documentation](https://docs.spring.io/spring-data/redis/reference/) -* [Valkey GLIDE Client](https://github.com/valkey-io/valkey-glide) +* [Valkey GLIDE Java Client Reference](https://glide.valkey.io/languages/java/) +* [Spring Data Reference](https://spring.io/projects/spring-data) * [Spring Boot Reference](https://docs.spring.io/spring-boot/reference/) diff --git a/MIGRATION.md b/MIGRATION.md index d0919ace8..24c830460 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -444,7 +444,7 @@ $ find path/to/project -type f \( -name "*.java" -o -name "*.properties" -o -nam ## Additional Resources -- [Valkey Documentation](https://valkey.io/docs/) -- [Valkey GLIDE](https://github.com/valkey-io/valkey-glide) -- [Spring Data Redis Documentation](https://docs.spring.io/spring-data/redis/reference/) -- [Spring Data Redis](https://github.com/spring-projects/spring-data-redis) +- [Valkey GLIDE Java Client Reference](https://glide.valkey.io/languages/java/) +- [Spring Data Valkey Reference](https://spring.valkey.io/) +- [Spring Data Redis Reference](https://docs.spring.io/spring-data/redis/reference/) +- [Spring Boot Reference](https://docs.spring.io/spring-boot/reference/) diff --git a/README.md b/README.md index 07ad467cc..3700f3ba9 100644 --- a/README.md +++ b/README.md @@ -131,8 +131,7 @@ If you're migrating from Spring Data Redis, see the [Migration Guide](MIGRATION. ## Documentation -For general usage patterns and API documentation, refer to: -* [Spring Data Redis Reference Documentation](https://docs.spring.io/spring-data/redis/reference/) - Most concepts apply to Valkey +* [Spring Data Valkey Reference](https://spring.valkey.io/) - For general usage patterns and API documentation * [Valkey Documentation](https://valkey.io/docs/) - For Valkey-specific features and commands ## License From 6c21776599fbcc01691a869a1f15854c293b1c6b Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 30 Jan 2026 10:46:22 -0800 Subject: [PATCH 12/21] Update schema location references Signed-off-by: Jeremy Parr-Pearson --- docs/src/content/docs/valkey/pubsub.mdx | 2 +- .../io/valkey/springframework/data/valkey/config/namespace.xml | 2 +- .../data/valkey/support/collections/container.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/valkey/pubsub.mdx b/docs/src/content/docs/valkey/pubsub.mdx index 708acaf90..2846c412a 100644 --- a/docs/src/content/docs/valkey/pubsub.mdx +++ b/docs/src/content/docs/valkey/pubsub.mdx @@ -145,7 +145,7 @@ class MyConfig { xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:valkey="http://www.springframework.org/schema/valkey" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/valkey https://www.springframework.org/schema/valkey/spring-valkey.xsd"> + http://www.springframework.org/schema/valkey https://spring.valkey.io/schema/valkey/spring-valkey-1.0.xsd"> diff --git a/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/config/namespace.xml b/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/config/namespace.xml index e969e84fe..6ab452be8 100644 --- a/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/config/namespace.xml +++ b/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/config/namespace.xml @@ -5,7 +5,7 @@ xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd - http://www.springframework.org/schema/valkey https://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd"> + http://www.springframework.org/schema/valkey https://spring.valkey.io/schema/valkey/spring-valkey-1.0.xsd"> diff --git a/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/support/collections/container.xml b/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/support/collections/container.xml index 6abbc8627..148e8bf38 100644 --- a/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/support/collections/container.xml +++ b/spring-data-valkey/src/test/resources/io/valkey/springframework/data/valkey/support/collections/container.xml @@ -4,7 +4,7 @@ xmlns:p="http://www.springframework.org/schema/p" xmlns:valkey="http://www.springframework.org/schema/valkey" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/valkey https://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd"> + http://www.springframework.org/schema/valkey https://spring.valkey.io/schema/valkey/spring-valkey-1.0.xsd"> From 52d31563c4a63b0eaab19df19107668778c0d5f0 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 30 Jan 2026 12:23:46 -0800 Subject: [PATCH 13/21] Fix CI lint errors Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/docs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2d4d2e72c..0f7ad775f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,15 +29,15 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" cache-dependency-path: docs/package-lock.json - name: Setup JDK uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '17' + java-version: "17" + distribution: "temurin" - name: Setup Pages uses: actions/configure-pages@v4 @@ -46,7 +46,7 @@ jobs: run: npm ci working-directory: ./docs - - name: Generate Javadocs + - name: Generate JavaDocs run: | ./mvnw javadoc:javadoc -pl spring-data-valkey -DskipTests mkdir -p docs/dist/api/java From 1741bdf517a5c4fbe052af0fea25fe8bfd996740 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 30 Jan 2026 12:44:10 -0800 Subject: [PATCH 14/21] Update spring.schemas lookup Signed-off-by: Jeremy Parr-Pearson --- spring-data-valkey/src/main/resources/META-INF/spring.schemas | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-data-valkey/src/main/resources/META-INF/spring.schemas b/spring-data-valkey/src/main/resources/META-INF/spring.schemas index 16b7e0c8d..d69405431 100644 --- a/spring-data-valkey/src/main/resources/META-INF/spring.schemas +++ b/spring-data-valkey/src/main/resources/META-INF/spring.schemas @@ -2,3 +2,5 @@ http\://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd=io/valkey/sp http\://www.springframework.org/schema/valkey/spring-valkey.xsd=io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd https\://www.springframework.org/schema/valkey/spring-valkey-1.0.xsd=io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd https\://www.springframework.org/schema/valkey/spring-valkey.xsd=io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd +https\://spring.valkey.io/schema/valkey/spring-valkey-1.0.xsd=io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd +https\://spring.valkey.io/schema/valkey/spring-valkey.xsd=io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd From bf8497e690bd8874cbd30eff930e2ff962401e75 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 4 Feb 2026 10:44:31 -0800 Subject: [PATCH 15/21] Address feedback Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/docs.yml | 11 ++-- docs/astro.config.mjs | 7 ++- ...spring-data-valkey-logo-with-name-dark.svg | 59 ++++++++++++++++++ ...pring-data-valkey-logo-with-name-light.svg | 62 +++++++++++++++++++ docs/src/assets/valkey-logo.svg | 7 +++ docs/src/content/docs/commons/spring-boot.md | 23 +++++++ docs/src/content/docs/index.mdx | 3 + docs/src/content/docs/overview.md | 2 +- docs/src/content/docs/valkey/cluster.md | 4 +- docs/src/content/docs/valkey/pipelining.md | 4 +- 10 files changed, 170 insertions(+), 12 deletions(-) create mode 100755 docs/src/assets/spring-data-valkey-logo-with-name-dark.svg create mode 100755 docs/src/assets/spring-data-valkey-logo-with-name-light.svg create mode 100755 docs/src/assets/valkey-logo.svg create mode 100644 docs/src/content/docs/commons/spring-boot.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0f7ad775f..7ab23441f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,8 +6,8 @@ on: # push: # branches: [main] # paths: - # - 'docs/**' - # - 'spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd' + # - "docs/**"" + # - "spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd" permissions: contents: read @@ -49,8 +49,11 @@ jobs: - name: Generate JavaDocs run: | ./mvnw javadoc:javadoc -pl spring-data-valkey -DskipTests - mkdir -p docs/dist/api/java - cp -r spring-data-valkey/target/site/apidocs/* docs/dist/api/java/ + ./mvnw javadoc:javadoc -pl spring-boot-starter-data-valkey -DskipTests + mkdir -p docs/dist/spring-data-valkey/api/java + mkdir -p docs/dist/spring-boot-starter-data-valkey/api/java + cp -r spring-data-valkey/target/site/apidocs/* docs/dist/spring-data-valkey/api/java/ + cp -r spring-boot-starter-data-valkey/target/site/apidocs/* docs/dist/spring-boot-starter-data-valkey/api/java/ - name: Copy Schema run: | diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index eb15b2798..73371662c 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -19,8 +19,8 @@ export default defineConfig({ starlight({ title: 'Spring Data Valkey', logo: { - light: './src/assets/valkey-logo-with-name-light.svg', - dark: './src/assets/valkey-logo-with-name-dark.svg', + light: './src/assets/spring-data-valkey-logo-with-name-light.svg', + dark: './src/assets/spring-data-valkey-logo-with-name-dark.svg', replacesTitle: true, }, customCss: ['./src/styles/custom.css', './src/styles/code-wrap.css'], @@ -33,6 +33,7 @@ export default defineConfig({ label: 'Overview', items: [ { label: 'Spring Data Valkey', slug: 'overview' }, + { label: 'Spring Boot', slug: 'commons/spring-boot' }, { label: 'Migrating Spring Data', slug: 'commons/migration' }, ] }, @@ -84,7 +85,7 @@ export default defineConfig({ { label: 'Observability', slug: 'observability' }, { label: 'Appendix', slug: 'appendix' }, { label: 'Valkey Project ↗', link: 'https://valkey.io/', attrs: { target: '_blank' } }, - { label: 'Javadoc ↗', link: 'https://spring.valkey.io/api/java/index.html', attrs: { target: '_blank' } }, + { label: 'Javadoc ↗', link: 'https://spring.valkey.io/spring-data-valkey/api/java/index.html', attrs: { target: '_blank' } }, ], }), ], diff --git a/docs/src/assets/spring-data-valkey-logo-with-name-dark.svg b/docs/src/assets/spring-data-valkey-logo-with-name-dark.svg new file mode 100755 index 000000000..853d69dd1 --- /dev/null +++ b/docs/src/assets/spring-data-valkey-logo-with-name-dark.svg @@ -0,0 +1,59 @@ + + + + + + + + Spring Data Valkey + + + + + diff --git a/docs/src/assets/spring-data-valkey-logo-with-name-light.svg b/docs/src/assets/spring-data-valkey-logo-with-name-light.svg new file mode 100755 index 000000000..d8ea68608 --- /dev/null +++ b/docs/src/assets/spring-data-valkey-logo-with-name-light.svg @@ -0,0 +1,62 @@ + + + + + + + + Spring Data Valkey + + + + + diff --git a/docs/src/assets/valkey-logo.svg b/docs/src/assets/valkey-logo.svg new file mode 100755 index 000000000..92e35ccb1 --- /dev/null +++ b/docs/src/assets/valkey-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/src/content/docs/commons/spring-boot.md b/docs/src/content/docs/commons/spring-boot.md new file mode 100644 index 000000000..048c8a825 --- /dev/null +++ b/docs/src/content/docs/commons/spring-boot.md @@ -0,0 +1,23 @@ +--- +title: Spring Boot +description: Spring Boot integration with Spring Data Valkey +--- + +Spring Boot provides auto-configuration support for Spring Data Valkey through a Spring Boot Starter for Valkey. This starter automatically configures Valkey connections, templates, repositories, and caching with sensible defaults while allowing full customization when needed. + +For complete documentation on Spring Boot, see the [Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/). + +# Spring Boot Starter Data Valkey + +The following auto-configuration classes are from the *spring-boot-data-valkey* module: + +| Configuration Class | Links | +|---------------------|-------| +| DataValkeyAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyAutoConfiguration.html) | +| DataValkeyHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyHealthContributorAutoConfiguration.html) | +| DataValkeyReactiveAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyReactiveAutoConfiguration.html) | +| DataValkeyReactiveHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyReactiveHealthContributorAutoConfiguration.html) | +| DataValkeyRepositoriesAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyRepositoriesAutoConfiguration.html) | +| LettuceObservationAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/LettuceObservationAutoConfiguration.html) | + +For more information on the Spring Data Valkey Spring Boot Starter, see the [GitHub repository](https://github.com/valkey-io/spring-data-valkey/tree/main/spring-boot-starter-data-valkey). diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index bfbc1411b..620c304db 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -3,7 +3,10 @@ title: Spring Data Valkey description: Spring Data Valkey provides Valkey connectivity and repository support for the Valkey database. It eases development of applications with a consistent programming model that need to access Valkey data sources. template: splash hero: + title: Spring Data Valkey tagline: Valkey connectivity and repository support for Spring applications + image: + file: ../../assets/valkey-logo.svg actions: - text: Overview link: /overview/ diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index 5a3837399..0490f862b 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -42,7 +42,7 @@ _Spring Data Valkey provides Valkey connectivity and repository support for the Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch, Jay Bryant *Spring Data Valkey Contributors:* -Jeremy Parr-Pearson +Ilia Kolominsky, Jeremy Parr-Pearson, Lior Sventitzky ## Copyright diff --git a/docs/src/content/docs/valkey/cluster.md b/docs/src/content/docs/valkey/cluster.md index 22d3e2f89..d8197c4c9 100644 --- a/docs/src/content/docs/valkey/cluster.md +++ b/docs/src/content/docs/valkey/cluster.md @@ -14,7 +14,7 @@ When using [Valkey Repositories](/repositories) with Valkey Cluster, make yourse Do not rely on keyspace events when using Valkey Cluster as keyspace events are not replicated across shards. ::: -Pub/Sub [subscribes to a random cluster node](https://github.com/valkey-io/spring-data-valkey/issues/1111) which only receives keyspace events from a single shard. +Pub/Sub [subscribes to a random cluster node](https://github.com/spring-projects/spring-data-redis/issues/1111) which only receives keyspace events from a single shard. Use single-node Valkey to avoid keyspace event loss. ## Working With Valkey Cluster Connection @@ -140,5 +140,5 @@ clusterOps.shutdown(NODE_7379); // ``` :::note -Valkey Cluster pipelining is currently only supported through the Lettuce driver. Valkey GLIDE and Jedis do not support pipelining in cluster mode. The Lettuce driver has exceptions for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Valkey Cluster pipelining is supported through the Valkey GLIDE and Lettuce drivers. Jedis does not support pipelining in cluster mode. The Lettuce driver has exceptions for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. ::: diff --git a/docs/src/content/docs/valkey/pipelining.md b/docs/src/content/docs/valkey/pipelining.md index a5a7e842b..ae3e2c9ac 100644 --- a/docs/src/content/docs/valkey/pipelining.md +++ b/docs/src/content/docs/valkey/pipelining.md @@ -40,8 +40,8 @@ factory.setPipeliningFlushPolicy(PipeliningFlushPolicy.buffered(3)); // (1) ``` :::note -Pipelining is limited to Valkey Standalone. +Pipelining is supported in Valkey Standalone and Cluster modes. ::: -Valkey Cluster is currently only supported through the Lettuce driver except for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. +Valkey Cluster pipelining is supported through the Valkey GLIDE and Lettuce drivers. Jedis does not support pipelining in cluster mode. The Lettuce driver has exceptions for the following commands when using cross-slot keys: `rename`, `renameNX`, `sort`, `bLPop`, `bRPop`, `rPopLPush`, `bRPopLPush`, `info`, `sMove`, `sInter`, `sInterStore`, `sUnion`, `sUnionStore`, `sDiff`, `sDiffStore`. Same-slot keys are fully supported. From 82a9aacdb4a71eef8c2fad6f21f6ebadffca4d19 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 4 Feb 2026 14:55:10 -0800 Subject: [PATCH 16/21] Fix docs workflow cache Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/docs.yml | 4 +- .gitignore | 2 - docs/package-lock.json | 6425 ++++++++++++++++++++++++++++++++++++ docs/package.json | 18 + 4 files changed, 6445 insertions(+), 4 deletions(-) create mode 100644 docs/package-lock.json create mode 100644 docs/package.json diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7ab23441f..d270ae75d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ on: # push: # branches: [main] # paths: - # - "docs/**"" + # - "docs/**" # - "spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd" permissions: @@ -31,7 +31,7 @@ jobs: with: node-version: "20" cache: "npm" - cache-dependency-path: docs/package-lock.json + cache-dependency-path: "docs/package-lock.json" - name: Setup JDK uses: actions/setup-java@v4 diff --git a/.gitignore b/.gitignore index 5c8826076..8d69a9a28 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,5 @@ appendonlydir build/ node_modules node -package-lock.json -package.json .mvn/.develocity .flattened-pom.xml diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..3e2bb1cb3 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,6425 @@ +{ + "name": "docs", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "0.0.1", + "dependencies": { + "@astrojs/starlight": "^0.37.3", + "astro": "^5.6.1", + "rehype-external-links": "^3.0.0", + "sharp": "^0.34.2" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", + "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz", + "integrity": "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==", + "license": "MIT" + }, + "node_modules/@astrojs/markdown-remark": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.10.tgz", + "integrity": "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.19.0", + "smol-toml": "^1.5.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-4.3.13.tgz", + "integrity": "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "6.3.10", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.15.0", + "es-module-lexer": "^1.7.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "astro": "^5.0.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", + "integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.0.tgz", + "integrity": "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA==", + "license": "MIT", + "dependencies": { + "sitemap": "^8.0.2", + "stream-replace-string": "^2.0.0", + "zod": "^3.25.76" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.37.3", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.37.3.tgz", + "integrity": "sha512-p7cqbAkBYkBTiK1NIomxAEoF9Wko+mTV503qDm5Wgh+0MGGJnSsIzCSSJ+rWm8toFk9mnzNwNbxcnjwzIBEU3w==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "^6.3.1", + "@astrojs/mdx": "^4.2.3", + "@astrojs/sitemap": "^3.3.0", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.41.1", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.1", + "hast-util-select": "^6.0.2", + "hast-util-to-string": "^3.0.0", + "hastscript": "^9.0.0", + "i18next": "^23.11.5", + "js-yaml": "^4.1.0", + "klona": "^2.0.6", + "magic-string": "^0.30.17", + "mdast-util-directive": "^3.0.0", + "mdast-util-to-markdown": "^2.1.0", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.3.0", + "rehype": "^13.0.1", + "rehype-format": "^5.0.0", + "remark-directive": "^3.0.0", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.2" + }, + "peerDependencies": { + "astro": "^5.5.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.41.6.tgz", + "integrity": "sha512-FvJQP+hG0jWi/FLBSmvHInDqWR7jNANp9PUDjdMqSshHb0y7sxx3vHuoOr6SgXjWw+MGLqorZyPQ0aAlHEok6g==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.41.6.tgz", + "integrity": "sha512-d+hkSYXIQot6fmYnOmWAM+7TNWRv/dhfjMsNq+mIZz8Tb4mPHOcgcfZeEM5dV9TDL0ioQNvtcqQNuzA1sRPjxg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.6" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.41.6.tgz", + "integrity": "sha512-Y6zmKBmsIUtWTzdefqlzm/h9Zz0Rc4gNdt2GTIH7fhHH2I9+lDYCa27BDwuBhjqcos6uK81Aca9dLUC4wzN+ng==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.6", + "shiki": "^3.2.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.41.6.tgz", + "integrity": "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.6" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", + "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", + "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.4.0.tgz", + "integrity": "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", + "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", + "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", + "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", + "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "5.16.11", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz", + "integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/markdown-remark": "6.3.10", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.3.1", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.1.1", + "cssesc": "^3.0.0", + "debug": "^4.4.3", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.6.2", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.25.0", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.3", + "shiki": "^3.20.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.1", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.3", + "vfile": "^6.0.3", + "vite": "^6.4.1", + "vitefu": "^1.1.1", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.41.6.tgz", + "integrity": "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.41.6" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "license": "ISC" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", + "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==", + "license": "MIT", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", + "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.41.6.tgz", + "integrity": "sha512-W/5+IQbrpCIM5KGLjO35wlp1NCwDOOVQb+PAvzEoGkW1xjGM807ZGfBKptNWH6UECvt6qgmLyWolCMYKh7eQmA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.41.6", + "@expressive-code/plugin-frames": "^0.41.6", + "@expressive-code/plugin-shiki": "^0.41.6", + "@expressive-code/plugin-text-markers": "^0.41.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.0.tgz", + "integrity": "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + } + }, + "node_modules/fontkitten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.2.tgz", + "integrity": "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", + "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.4.0", + "@pagefind/darwin-x64": "1.4.0", + "@pagefind/freebsd-x64": "1.4.0", + "@pagefind/linux-arm64": "1.4.0", + "@pagefind/linux-x64": "1.4.0", + "@pagefind/windows-x64": "1.4.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-expressive-code": { + "version": "0.41.6", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.41.6.tgz", + "integrity": "sha512-aBMX8kxPtjmDSFUdZlAWJkMvsQ4ZMASfee90JWIAV8tweltXLzkWC3q++43ToTelI8ac5iC0B3/S/Cl4Ql1y2g==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.41.6" + } + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz", + "integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.3.tgz", + "integrity": "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", + "integrity": "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.5", + "lru-cache": "^11.2.0", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/valkey": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/valkey": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-spinner": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.3.tgz", + "integrity": "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz", + "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..1eff556a8 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,18 @@ +{ + "name": "docs", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/starlight": "^0.37.3", + "astro": "^5.6.1", + "rehype-external-links": "^3.0.0", + "sharp": "^0.34.2" + } +} From bde0db5233da83d97f20fd28910bde3e0a832760 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 5 Feb 2026 08:43:28 -0800 Subject: [PATCH 17/21] Fix workflow JavaDocs dir Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/docs.yml | 12 ++++++------ docs/astro.config.mjs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d270ae75d..07f2f8337 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,24 +46,24 @@ jobs: run: npm ci working-directory: ./docs + - name: Build Astro docs + run: npm run build + working-directory: ./docs + - name: Generate JavaDocs run: | ./mvnw javadoc:javadoc -pl spring-data-valkey -DskipTests ./mvnw javadoc:javadoc -pl spring-boot-starter-data-valkey -DskipTests mkdir -p docs/dist/spring-data-valkey/api/java mkdir -p docs/dist/spring-boot-starter-data-valkey/api/java - cp -r spring-data-valkey/target/site/apidocs/* docs/dist/spring-data-valkey/api/java/ - cp -r spring-boot-starter-data-valkey/target/site/apidocs/* docs/dist/spring-boot-starter-data-valkey/api/java/ + cp -r spring-data-valkey/target/reports/apidocs/* docs/dist/spring-data-valkey/api/java/ + cp -r spring-boot-starter-data-valkey/target/reports/apidocs/* docs/dist/spring-boot-starter-data-valkey/api/java/ - name: Copy Schema run: | mkdir -p docs/dist/schema/valkey cp spring-data-valkey/src/main/resources/io/valkey/springframework/data/valkey/config/spring-valkey-1.0.xsd docs/dist/schema/valkey/ - - name: Build docs - run: npm run build - working-directory: ./docs - - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 73371662c..7567e42fe 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -5,11 +5,10 @@ import rehypeExternalLinks from 'rehype-external-links'; // https://astro.build/config export default defineConfig({ - // TODO: Once CNAME record is added, change site and remove base property, - // and rename docs/public/CNAME.example to docs/public/CNAME + // TODO: Once CNAME record is added, update site/base and rename docs/public/CNAME.example //site: 'https://spring.valkey.io', - site: 'https://valkey-io.github.io', - base: process.env.NODE_ENV === 'production' ? '/spring-data-valkey' : '/', + site: 'https://valkey.io', + base: '/spring-data-valkey', markdown: { rehypePlugins: [ [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }] @@ -86,6 +85,7 @@ export default defineConfig({ { label: 'Appendix', slug: 'appendix' }, { label: 'Valkey Project ↗', link: 'https://valkey.io/', attrs: { target: '_blank' } }, { label: 'Javadoc ↗', link: 'https://spring.valkey.io/spring-data-valkey/api/java/index.html', attrs: { target: '_blank' } }, + { label: 'Spring Boot Javadoc ↗', link: 'https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/index.html', attrs: { target: '_blank' } }, ], }), ], From bcefb204a3f37c41e6d69f71376f327597c0aeb3 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 5 Feb 2026 10:09:59 -0800 Subject: [PATCH 18/21] Use spring.valkey.io CNAME Signed-off-by: Jeremy Parr-Pearson --- docs/astro.config.mjs | 5 +---- docs/public/{CNAME.example => CNAME} | 0 2 files changed, 1 insertion(+), 4 deletions(-) rename docs/public/{CNAME.example => CNAME} (100%) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 7567e42fe..1c923c245 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -5,10 +5,7 @@ import rehypeExternalLinks from 'rehype-external-links'; // https://astro.build/config export default defineConfig({ - // TODO: Once CNAME record is added, update site/base and rename docs/public/CNAME.example - //site: 'https://spring.valkey.io', - site: 'https://valkey.io', - base: '/spring-data-valkey', + site: 'https://spring.valkey.io', markdown: { rehypePlugins: [ [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }] diff --git a/docs/public/CNAME.example b/docs/public/CNAME similarity index 100% rename from docs/public/CNAME.example rename to docs/public/CNAME From e6e77b195ad90cc4960802bf492300c03678ff8a Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 5 Feb 2026 10:46:20 -0800 Subject: [PATCH 19/21] Fix JavaDoc links Signed-off-by: Jeremy Parr-Pearson --- docs/src/content/docs/commons/spring-boot.md | 12 ++++++------ docs/src/content/docs/overview.md | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/src/content/docs/commons/spring-boot.md b/docs/src/content/docs/commons/spring-boot.md index 048c8a825..7f7d42330 100644 --- a/docs/src/content/docs/commons/spring-boot.md +++ b/docs/src/content/docs/commons/spring-boot.md @@ -13,11 +13,11 @@ The following auto-configuration classes are from the *spring-boot-data-valkey* | Configuration Class | Links | |---------------------|-------| -| DataValkeyAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyAutoConfiguration.html) | -| DataValkeyHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyHealthContributorAutoConfiguration.html) | -| DataValkeyReactiveAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyReactiveAutoConfiguration.html) | -| DataValkeyReactiveHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyReactiveHealthContributorAutoConfiguration.html) | -| DataValkeyRepositoriesAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/DataValkeyRepositoriesAutoConfiguration.html) | -| LettuceObservationAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/LettuceObservationAutoConfiguration.html) | +| ValkeyAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/ValkeyAutoConfiguration.html) | +| ValkeyHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/actuate/autoconfigure/data/valkey/ValkeyHealthContributorAutoConfiguration.html) | +| ValkeyReactiveAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/ValkeyReactiveAutoConfiguration.html) | +| ValkeyReactiveHealthContributorAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/actuate/autoconfigure/data/valkey/ValkeyReactiveHealthContributorAutoConfiguration.html) | +| ValkeyRepositoriesAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/autoconfigure/data/valkey/ValkeyRepositoriesAutoConfiguration.html) | +| ValkeyLettuceMetricsAutoConfiguration | [javadoc](https://spring.valkey.io/spring-boot-starter-data-valkey/api/java/io/valkey/springframework/boot/actuate/autoconfigure/metrics/valkey/ValkeyLettuceMetricsAutoConfiguration.html) | For more information on the Spring Data Valkey Spring Boot Starter, see the [GitHub repository](https://github.com/valkey-io/spring-data-valkey/tree/main/spring-boot-starter-data-valkey). diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index 0490f862b..105b40e05 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -33,6 +33,14 @@ _Spring Data Valkey provides Valkey connectivity and repository support for the Valkey Project ↗ Official Valkey project site and community resources + +Spring Data Valkey API ↗ +Core library Javadoc documentation + + +Spring Boot Starter API ↗ +Spring Boot auto-configuration Javadoc documentation + From 230f136e0867578a2dd7b7bd15649a827ec75d9d Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Thu, 19 Feb 2026 09:33:09 -0800 Subject: [PATCH 20/21] Add notice file for modifications Signed-off-by: Jeremy Parr-Pearson --- NOTICE.txt | 7 +++++++ README.md | 2 +- docs/src/content/docs/overview.md | 16 ++-------------- 3 files changed, 10 insertions(+), 15 deletions(-) create mode 100644 NOTICE.txt diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 000000000..7645e5abb --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,7 @@ +Spring Data Valkey +Copyright 2025-2026 Amazon.com, Inc. or its affiliates + +This product is a derivative work based on Spring Data Redis. +Original work Copyright 2008-2024 VMware, Inc. + +This product includes software developed by VMware, Inc. (https://spring.io/) diff --git a/README.md b/README.md index 3700f3ba9..7ef4fe193 100644 --- a/README.md +++ b/README.md @@ -136,4 +136,4 @@ If you're migrating from Spring Data Redis, see the [Migration Guide](MIGRATION. ## License -Spring Data Valkey is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). +Spring Data Valkey is Open Source software released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt) and [NOTICE.txt](NOTICE.txt) for details. diff --git a/docs/src/content/docs/overview.md b/docs/src/content/docs/overview.md index 105b40e05..8deb019f2 100644 --- a/docs/src/content/docs/overview.md +++ b/docs/src/content/docs/overview.md @@ -44,18 +44,6 @@ _Spring Data Valkey provides Valkey connectivity and repository support for the -## Authors +## License -*Original Spring Data Redis Authors:* -Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch, Jay Bryant - -*Spring Data Valkey Contributors:* -Ilia Kolominsky, Jeremy Parr-Pearson, Lior Sventitzky - -## Copyright - -(C) 2008-2024 VMware, Inc. - -(C) 2025-2026 Valkey Contributors - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. +Licensed under the Apache License, Version 2.0. See [LICENSE.txt](https://github.com/valkey-io/spring-data-valkey/blob/main/LICENSE.txt) and [NOTICE.txt](https://github.com/valkey-io/spring-data-valkey/blob/main/NOTICE.txt) for details. From 330b2be88ebba7655bb13ec8822e51331a9e34ac Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Tue, 10 Mar 2026 11:53:56 -0700 Subject: [PATCH 21/21] Fix package names and add features page Signed-off-by: Jeremy Parr-Pearson --- DEVELOPER.md | 14 ---- docs/astro.config.mjs | 1 + docs/src/content/docs/commons/features.md | 45 +++++++++++++ docs/src/content/docs/commons/migration.md | 4 +- docs/src/content/docs/observability.md | 4 +- .../content/docs/valkey/connection-modes.md | 10 +-- docs/src/content/docs/valkey/drivers.md | 17 ++--- .../content/docs/valkey/getting-started.md | 8 +-- docs/src/content/docs/valkey/hash-mappers.md | 14 ++-- docs/src/content/docs/valkey/pubsub.mdx | 24 +++---- docs/src/content/docs/valkey/scripting.mdx | 8 +-- .../content/docs/valkey/support-classes.mdx | 10 +-- docs/src/content/docs/valkey/template.mdx | 66 +++++++++---------- docs/src/content/docs/valkey/transactions.md | 4 +- docs/src/content/docs/valkey/valkey-cache.md | 10 +-- .../valkey-repositories/cdi-integration.md | 6 +- .../valkey/valkey-repositories/expirations.md | 8 +-- .../src/content/docs/valkey/valkey-streams.md | 8 +-- 18 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 docs/src/content/docs/commons/features.md diff --git a/DEVELOPER.md b/DEVELOPER.md index 58307f0c1..b7a8d929e 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -9,7 +9,6 @@ Spring Data Valkey is organized as a multi-module Maven project: * **[`spring-data-valkey`](spring-data-valkey/)** - Core Spring Data Valkey library * **[`spring-boot-starter-data-valkey`](spring-boot-starter-data-valkey/)** - Spring Boot starter for auto-configuration * **[`examples`](examples/)** - Example applications demonstrating various Spring Data features -* **[`performance`](performance/)** - Performance testing and benchmarking tools ## Prerequisites @@ -99,19 +98,6 @@ $ ./mvnw -q exec:java -pl examples/spring-boot For detailed information about all available examples and their specific features, see the [examples](examples/) directory. -## Performance Testing - -```bash -# Default performance test with infrastructure management -$ make performance - -# Test with different clients against existing Valkey instance -$ ./mvnw -q exec:java -pl performance -Dclient=valkeyglide -$ ./mvnw -q exec:java -pl performance -Dclient=lettuce -``` - -For detailed information about all available performance tests and benchmarking options, see the [performance](performance/) directory. - ## Logging Configuration ### Spring Boot diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 1c923c245..89b8bf2b8 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -30,6 +30,7 @@ export default defineConfig({ items: [ { label: 'Spring Data Valkey', slug: 'overview' }, { label: 'Spring Boot', slug: 'commons/spring-boot' }, + { label: 'Features', slug: 'commons/features' }, { label: 'Migrating Spring Data', slug: 'commons/migration' }, ] }, diff --git a/docs/src/content/docs/commons/features.md b/docs/src/content/docs/commons/features.md new file mode 100644 index 000000000..0cb442ed8 --- /dev/null +++ b/docs/src/content/docs/commons/features.md @@ -0,0 +1,45 @@ +--- +title: Features +description: What Spring Data Valkey offers beyond Spring Data Redis +--- + +Spring Data Valkey provides first-class support for Valkey with enhanced capabilities through the [Valkey GLIDE](https://github.com/valkey-io/valkey-glide) driver. + +## What's New in Spring Data Valkey + +### Valkey GLIDE Driver Support + +Spring Data Valkey is the first Spring Data module to support Valkey GLIDE, a high-performance client library purpose-built for Valkey. GLIDE is the recommended driver and provides capabilities not available in traditional Redis clients. + +See the [Drivers](/valkey/drivers) page for a complete feature comparison across all supported drivers. + +### Availability Zone Awareness + +AZ-aware reads for cluster deployments allow read operations to prefer replicas in the same availability zone, reducing cross-AZ data transfer costs and improving latency. + +### IAM Authentication (AWS) + +Native support for AWS IAM authentication with Amazon ElastiCache and MemoryDB. The client automatically generates and refreshes short-lived IAM tokens, eliminating static passwords. *(Available in upcoming release)* + +### Native OpenTelemetry Support + +Built-in OpenTelemetry instrumentation for GLIDE enables automatic trace and metric export through Spring Boot properties—no code changes required. + +### Pub/Sub Support + +GLIDE provides pub/sub support with callback-based message delivery configured at client creation time. The driver supports configurable reconnection strategies through `BackoffStrategy`. + +### High-Performance I/O + +GLIDE's internal connection pooling and async operations are optimized for high-throughput scenarios, delivering improved performance for demanding workloads. + +## Spring Boot Enhancements + +* Property-based IAM authentication configuration +* Property-based OpenTelemetry configuration for GLIDE +* Auto-configuration for all three drivers (GLIDE, Lettuce, Jedis) +* Enhanced Actuator metrics for GLIDE connections + +## Migration from Spring Data Redis + +Spring Data Valkey maintains API compatibility with Spring Data Redis. Migration primarily involves updating dependencies and package names. See the [Migration Guide](/commons/migration) for details. diff --git a/docs/src/content/docs/commons/migration.md b/docs/src/content/docs/commons/migration.md index 4f005a0b2..4460f0d0c 100644 --- a/docs/src/content/docs/commons/migration.md +++ b/docs/src/content/docs/commons/migration.md @@ -3,12 +3,12 @@ title: Migrating Spring Data description: Instructions for migrating from Spring Data Redis to Spring Data Valkey --- -Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive [Migration Guide](https://github.com/valkey-io/spring-data-valkey/blob/main/MIGRATION.md) in the Spring Data Valkey repository. +Spring Data Valkey is a fork of Spring Data Redis 3.5.1, created to provide first-class support for Valkey database. To migrate from Spring Data Redis to Spring Data Valkey, see the comprehensive [Migration Guide](https://github.com/valkey-io/spring-data-valkey/blob/main/MIGRATION.md) in the Spring Data Valkey repository. The migration guide covers: - **Dependency Changes** - Updated Maven/Gradle dependencies for Spring Boot and vanilla Spring -- **Package Name Changes** - Spring Data packages: `org.springframework.data.redis.*` → `io.valkey.springframework.data.*`, Spring Boot packages: `org.springframework.boot.*.redis.*` → `io.valkey.springframework.boot.*.valkey.*` +- **Package Name Changes** - Spring Data packages: `org.springframework.data.redis.*` → `io.valkey.springframework.data.valkey.*`, Spring Boot packages: `org.springframework.boot.*.redis.*` → `io.valkey.springframework.boot.*.valkey.*` - **Class Name Changes** - Redis classes renamed to Valkey equivalents (`RedisTemplate` → `ValkeyTemplate`, `@EnableRedisRepositories` → `@EnableValkeyRepositories`, etc.) - **Configuration Changes** - Updated property names and configuration classes - **Testing Changes** - Testcontainers and test configuration updates diff --git a/docs/src/content/docs/observability.md b/docs/src/content/docs/observability.md index 3ec4dc850..0879b6cb9 100644 --- a/docs/src/content/docs/observability.md +++ b/docs/src/content/docs/observability.md @@ -111,7 +111,7 @@ Below you can find a list of all metrics declared by this project. **Metric name** `spring.data.valkey`. **Type** `timer` and **base unit** `seconds`. -Fully qualified name of the enclosing class `io.valkey.springframework.data.connection.lettuce.observability.ValkeyObservation`. +Fully qualified name of the enclosing class `io.valkey.springframework.data.valkey.connection.lettuce.observability.ValkeyObservation`. *Table 1. Low cardinality Keys* @@ -144,7 +144,7 @@ Below you can find a list of all spans declared by this project. **Span name** `spring.data.valkey`. -Fully qualified name of the enclosing class `io.valkey.springframework.data.connection.lettuce.observability.ValkeyObservation`. +Fully qualified name of the enclosing class `io.valkey.springframework.data.valkey.connection.lettuce.observability.ValkeyObservation`. *Table 3. Tag Keys* diff --git a/docs/src/content/docs/valkey/connection-modes.md b/docs/src/content/docs/valkey/connection-modes.md index 4d631a344..731d6a8cf 100644 --- a/docs/src/content/docs/valkey/connection-modes.md +++ b/docs/src/content/docs/valkey/connection-modes.md @@ -10,7 +10,7 @@ Each mode of operation requires specific configuration that is explained in the The easiest way to get started is by using Valkey Standalone with a single Valkey server, -Configure `io.valkey.springframework.data.connection.valkeyglide.ValkeyGlideConnectionFactory`, `io.valkey.springframework.data.connection.lettuce.LettuceConnectionFactory` or `io.valkey.springframework.data.connection.jedis.JedisConnectionFactory`, as shown in the following example: +Configure `io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory`, `io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory` or `io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory`, as shown in the following example: ```java @Configuration @@ -67,12 +67,12 @@ class WriteToMasterReadFromReplicaConfiguration { ``` :::tip -For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `io.valkey.springframework.data.connection.ValkeyStaticMasterReplicaConfiguration` instead of `io.valkey.springframework.data.connection.ValkeyStandaloneConfiguration`. Please note that `ValkeyStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. +For environments reporting non-public addresses through the `INFO` command (for example, when using AWS), use `io.valkey.springframework.data.valkey.connection.ValkeyStaticMasterReplicaConfiguration` instead of `io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration`. Please note that `ValkeyStaticMasterReplicaConfiguration` does not support Pub/Sub because of missing Pub/Sub message propagation across individual servers. ::: ## Valkey Sentinel -For dealing with high-availability Valkey, Spring Data Valkey has support for [Valkey Sentinel](https://valkey.io/topics/sentinel), using `io.valkey.springframework.data.connection.ValkeySentinelConfiguration`, as shown in the following example: +For dealing with high-availability Valkey, Spring Data Valkey has support for [Valkey Sentinel](https://valkey.io/topics/sentinel), using `io.valkey.springframework.data.valkey.connection.ValkeySentinelConfiguration`, as shown in the following example: :::note[Driver Support] Valkey Sentinel is currently supported by Lettuce and Jedis drivers. Valkey GLIDE support for Sentinel is planned for a future release. @@ -122,8 +122,8 @@ Sometimes, direct interaction with one of the Sentinels is required. Using `Valk ## Valkey Cluster -[Cluster support](/valkey/cluster) is based on the same building blocks as non-clustered communication. `io.valkey.springframework.data.connection.ValkeyClusterConnection`, an extension to `ValkeyConnection`, handles the communication with the Valkey Cluster and translates errors into the Spring DAO exception hierarchy. -`ValkeyClusterConnection` instances are created with the `ValkeyConnectionFactory`, which has to be set up with the associated `io.valkey.springframework.data.connection.ValkeyClusterConfiguration`, as shown in the following example: +[Cluster support](/valkey/cluster) is based on the same building blocks as non-clustered communication. `io.valkey.springframework.data.valkey.connection.ValkeyClusterConnection`, an extension to `ValkeyConnection`, handles the communication with the Valkey Cluster and translates errors into the Spring DAO exception hierarchy. +`ValkeyClusterConnection` instances are created with the `ValkeyConnectionFactory`, which has to be set up with the associated `io.valkey.springframework.data.valkey.connection.ValkeyClusterConfiguration`, as shown in the following example: *Example 1. Sample ValkeyConnectionFactory Configuration for Valkey Cluster* diff --git a/docs/src/content/docs/valkey/drivers.md b/docs/src/content/docs/valkey/drivers.md index 03200a69e..5fc533dd9 100644 --- a/docs/src/content/docs/valkey/drivers.md +++ b/docs/src/content/docs/valkey/drivers.md @@ -6,7 +6,7 @@ description: Drivers documentation One of the first tasks when using Valkey and Spring is to connect to the store through the IoC container. To do that, a Java connector (or binding) is required. No matter the library you choose, you need to use only one set of Spring Data Valkey APIs (which behaves consistently across all connectors). -The `io.valkey.springframework.data.connection` package and its `ValkeyConnection` and `ValkeyConnectionFactory` interfaces for working with and retrieving active connections to Valkey. +The `io.valkey.springframework.data.valkey.connection` package and its `ValkeyConnection` and `ValkeyConnectionFactory` interfaces for working with and retrieving active connections to Valkey. ## ValkeyConnection and ValkeyConnectionFactory @@ -47,11 +47,12 @@ The following overview explains features that are supported by the individual Va | Standalone Connections | X | X | X | | [Master/Replica Connections](/valkey/connection-modes#write-to-master-read-from-replica) | X | X | X | | [Valkey Sentinel](/valkey/connection-modes#valkey-sentinel) | | Master Lookup, Sentinel Authentication, Replica Reads | Master Lookup | -| [Valkey Cluster](/valkey/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | +| [Valkey Cluster](/valkey/cluster) | Cluster Connections, Cluster Node Connections, Replica Reads, AZ-aware Reads | Cluster Connections, Cluster Node Connections, Replica Reads | Cluster Connections, Cluster Node Connections | | Transport Channels | TCP | TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets | TCP | -| Connection Pooling | X (using `LinkedBlockingQueue`) | X (using `commons-pool2`) | X (using `commons-pool2`) | +| Connection Pooling | X (using internal connection pool) | X (using `commons-pool2`) | X (using `commons-pool2`) | | Other Connection Features | High-performance async operations | Singleton-connection sharing for non-blocking commands | Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions. | | SSL Support | X | X | X | +| IAM Authentication (AWS) | X (ElastiCache, MemoryDB) | | | | [Pub/Sub](/valkey/pubsub) | X | X | X | | [Pipelining](/valkey/pipelining) | X | X | X (Pipelining and Transactions mutually exclusive) | | [Transactions](/valkey/transactions) | X | X | X (Pipelining and Transactions mutually exclusive) | @@ -60,7 +61,7 @@ The following overview explains features that are supported by the individual Va ## Configuring the Valkey GLIDE Connector -[Valkey GLIDE](https://github.com/valkey-io/valkey-glide) is a high-performance, cross-language client library for Valkey, supported by Spring Data Valkey through the `io.valkey.springframework.data.connection.valkeyglide` package. +[Valkey GLIDE](https://github.com/valkey-io/valkey-glide) is a high-performance, cross-language client library for Valkey, supported by Spring Data Valkey through the `io.valkey.springframework.data.valkey.connection.valkeyglide` package. *Add the following to the pom.xml files `dependencies` element:* @@ -121,11 +122,11 @@ public ValkeyGlideConnectionFactory valkeyGlideConnectionFactory() { } ``` -For more detailed client configuration options, see `io.valkey.springframework.data.connection.valkeyglide.ValkeyGlideClientConfiguration`. +For more detailed client configuration options, see `io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideClientConfiguration`. ## Configuring the Lettuce Connector -[Lettuce](https://github.com/lettuce-io/lettuce-core) is a [Netty](https://netty.io/)-based open-source connector supported by Spring Data Valkey through the `io.valkey.springframework.data.connection.lettuce` package. +[Lettuce](https://github.com/lettuce-io/lettuce-core) is a [Netty](https://netty.io/)-based open-source connector supported by Spring Data Valkey through the `io.valkey.springframework.data.valkey.connection.lettuce` package. *Add the following to the pom.xml files `dependencies` element:* @@ -177,7 +178,7 @@ public LettuceConnectionFactory lettuceConnectionFactory() { } ``` -For more detailed client configuration tweaks, see `io.valkey.springframework.data.connection.lettuce.LettuceClientConfiguration`. +For more detailed client configuration tweaks, see `io.valkey.springframework.data.valkey.connection.lettuce.LettuceClientConfiguration`. Lettuce integrates with Netty's [native transports](https://netty.io/wiki/native-transports.html), letting you use Unix domain sockets to communicate with Valkey. Make sure to include the appropriate native transport dependencies that match your runtime environment. @@ -201,7 +202,7 @@ Netty currently supports the epoll (Linux) and kqueue (BSD/macOS) interfaces for ## Configuring the Jedis Connector -[Jedis](https://github.com/redis/jedis) is a community-driven connector supported by the Spring Data Valkey module through the `io.valkey.springframework.data.connection.jedis` package. +[Jedis](https://github.com/redis/jedis) is a community-driven connector supported by the Spring Data Valkey module through the `io.valkey.springframework.data.valkey.connection.jedis` package. *Add the following to the pom.xml files `dependencies` element:* diff --git a/docs/src/content/docs/valkey/getting-started.md b/docs/src/content/docs/valkey/getting-started.md index b4961716f..caf87b7b1 100644 --- a/docs/src/content/docs/valkey/getting-started.md +++ b/docs/src/content/docs/valkey/getting-started.md @@ -20,9 +20,9 @@ Create the main application to run, as the following example shows: import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import io.valkey.springframework.data.connection.ValkeyConnectionFactory; -import io.valkey.springframework.data.connection.glide.ValkeyGlideConnectionFactory; -import io.valkey.springframework.data.core.ValkeyTemplate; +import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.valkey.connection.glide.ValkeyGlideConnectionFactory; +import io.valkey.springframework.data.valkey.core.ValkeyTemplate; public class ValkeyApplication { @@ -46,6 +46,6 @@ public class ValkeyApplication { ``` Even in this simple example, there are a few notable things to point out: -* You can create an instance of `io.valkey.springframework.data.core.ValkeyTemplate` with a `io.valkey.springframework.data.connection.ValkeyConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. +* You can create an instance of `io.valkey.springframework.data.valkey.core.ValkeyTemplate` with a `io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory`. Connection factories are an abstraction on top of the supported drivers. * For reactive programming with `ReactiveValkeyTemplate`, only Lettuce is supported. * There's no single way to use Valkey as it comes with support for a wide range of data structures such as plain keys ("strings"), lists, sets, sorted sets, streams, hashes and so on. diff --git a/docs/src/content/docs/valkey/hash-mappers.md b/docs/src/content/docs/valkey/hash-mappers.md index 9cddacde1..4da3789e7 100644 --- a/docs/src/content/docs/valkey/hash-mappers.md +++ b/docs/src/content/docs/valkey/hash-mappers.md @@ -3,20 +3,20 @@ title: Hash Mappers description: Hash Mappers documentation --- -Data can be stored by using various data structures within Valkey. `io.valkey.springframework.data.serializer.Jackson2JsonValkeySerializer` can convert objects in [JSON](https://en.wikipedia.org/wiki/JSON) format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Valkey hashes. Spring Data Valkey offers various strategies for mapping data to hashes (depending on the use case): +Data can be stored by using various data structures within Valkey. `io.valkey.springframework.data.valkey.serializer.Jackson2JsonValkeySerializer` can convert objects in [JSON](https://en.wikipedia.org/wiki/JSON) format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Valkey hashes. Spring Data Valkey offers various strategies for mapping data to hashes (depending on the use case): -* Direct mapping, by using `io.valkey.springframework.data.core.HashOperations` and a [serializer](/valkey/template#serializers) +* Direct mapping, by using `io.valkey.springframework.data.valkey.core.HashOperations` and a [serializer](/valkey/template#serializers) * Using [Valkey Repositories](/repositories) -* Using `io.valkey.springframework.data.hash.HashMapper` and `io.valkey.springframework.data.core.HashOperations` +* Using `io.valkey.springframework.data.valkey.hash.HashMapper` and `io.valkey.springframework.data.valkey.core.HashOperations` ## Hash Mappers -Hash mappers are converters of map objects to a `Map` and back. `io.valkey.springframework.data.hash.HashMapper` is intended for using with Valkey Hashes. +Hash mappers are converters of map objects to a `Map` and back. `io.valkey.springframework.data.valkey.hash.HashMapper` is intended for using with Valkey Hashes. Multiple implementations are available: -* `io.valkey.springframework.data.hash.BeanUtilsHashMapper` using Spring's [BeanUtils](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html). -* `io.valkey.springframework.data.hash.ObjectHashMapper` using [Object-to-Hash Mapping](/valkey/valkey-repositories/mapping). +* `io.valkey.springframework.data.valkey.hash.BeanUtilsHashMapper` using Spring's [BeanUtils](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html). +* `io.valkey.springframework.data.valkey.hash.ObjectHashMapper` using [Object-to-Hash Mapping](/valkey/valkey-repositories/mapping). * [`Jackson2HashMapper`](#jackson2hashmapper) using [FasterXML Jackson](https://github.com/FasterXML/jackson). The following example shows one way to implement hash mapping: @@ -52,7 +52,7 @@ public class HashMapping { ## Jackson2HashMapper -`io.valkey.springframework.data.hash.Jackson2HashMapper` provides Valkey Hash mapping for domain objects by using [FasterXML Jackson](https://github.com/FasterXML/jackson). +`io.valkey.springframework.data.valkey.hash.Jackson2HashMapper` provides Valkey Hash mapping for domain objects by using [FasterXML Jackson](https://github.com/FasterXML/jackson). `Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. diff --git a/docs/src/content/docs/valkey/pubsub.mdx b/docs/src/content/docs/valkey/pubsub.mdx index 2846c412a..1654ec246 100644 --- a/docs/src/content/docs/valkey/pubsub.mdx +++ b/docs/src/content/docs/valkey/pubsub.mdx @@ -14,7 +14,7 @@ Valkey messaging can be roughly divided into two areas of functionality: This is an example of the pattern often called Publish/Subscribe (Pub/Sub for short). The `ValkeyTemplate` class is used for message production. For asynchronous reception similar to Java EE's message-driven bean style, Spring Data provides a dedicated message listener container that is used to create Message-Driven POJOs (MDPs) and, for synchronous reception, the `ValkeyConnection` contract. -The `io.valkey.springframework.data.connection` and `io.valkey.springframework.data.listener` packages provide the core functionality for Valkey messaging. +The `io.valkey.springframework.data.valkey.connection` and `io.valkey.springframework.data.valkey.listener` packages provide the core functionality for Valkey messaging. ## Publishing (Sending Messages) @@ -69,19 +69,19 @@ In order to subscribe to messages, one needs to implement the `MessageListener` ### Message Listener Containers -Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). +Due to its blocking nature, low-level subscription is not attractive, as it requires connection and thread management for every single listener. To alleviate this problem, Spring Data offers `io.valkey.springframework.data.valkey.listener.ValkeyMessageListenerContainer`, which does all the heavy lifting. If you are familiar with EJB and JMS, you should find the concepts familiar, as it is designed to be as close as possible to the support in Spring Framework and its message-driven POJOs (MDPs). -`io.valkey.springframework.data.listener.ValkeyMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Valkey channel and drive the `io.valkey.springframework.data.connection.MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Valkey infrastructure concerns to the framework. +`io.valkey.springframework.data.valkey.listener.ValkeyMessageListenerContainer` acts as a message listener container. It is used to receive messages from a Valkey channel and drive the `io.valkey.springframework.data.valkey.connection.MessageListener` instances that are injected into it. The listener container is responsible for all threading of message reception and dispatches into the listener for processing. A message listener container is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Valkey infrastructure concerns to the framework. -A `io.valkey.springframework.data.connection.MessageListener` can additionally implement `io.valkey.springframework.data.connection.SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. +A `io.valkey.springframework.data.valkey.connection.MessageListener` can additionally implement `io.valkey.springframework.data.valkey.connection.SubscriptionListener` to receive notifications upon subscription/unsubscribe confirmation. Listening to subscription notifications can be useful when synchronizing invocations. -Furthermore, to minimize the application footprint, `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `ValkeyConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. +Furthermore, to minimize the application footprint, `io.valkey.springframework.data.valkey.listener.ValkeyMessageListenerContainer` lets one connection and one thread be shared by multiple listeners even though they do not share a subscription. Thus, no matter how many listeners or channels an application tracks, the runtime cost remains the same throughout its lifetime. Moreover, the container allows runtime configuration changes so that you can add or remove listeners while an application is running without the need for a restart. Additionally, the container uses a lazy subscription approach, using a `ValkeyConnection` only when needed. If all the listeners are unsubscribed, cleanup is automatically performed, and the thread is released. To help with the asynchronous nature of messages, the container requires a `java.util.concurrent.Executor` (or Spring's `TaskExecutor`) for dispatching the messages. Depending on the load, the number of listeners, or the runtime environment, you should change or tweak the executor to better serve your needs. In particular, in managed environments (such as app servers), it is highly recommended to pick a proper `TaskExecutor` to take advantage of its runtime. ### The MessageListenerAdapter -The `io.valkey.springframework.data.listener.adapter.MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). +The `io.valkey.springframework.data.valkey.listener.adapter.MessageListenerAdapter` class is the final component in Spring's asynchronous messaging support. In a nutshell, it lets you expose almost *any* class as a MDP (though there are some constraints). Consider the following interface definition: @@ -96,7 +96,7 @@ public interface MessageDelegate { } ``` -Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `io.valkey.springframework.data.listener.adapter.MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: +Notice that, although the interface does not extend the `MessageListener` interface, it can still be used as a MDP by using the `io.valkey.springframework.data.valkey.listener.adapter.MessageListenerAdapter` class. Notice also how the various message handling methods are strongly typed according to the *contents* of the various `Message` types that they can receive and handle. In addition, the channel or pattern to which a message is sent can be passed in to the method as the second argument of type `String`: ```java public class DefaultMessageDelegate implements MessageDelegate { @@ -168,18 +168,18 @@ The listener topic can be either a channel (for example, `topic="chatroom"` resp The preceding example uses the Valkey namespace to declare the message listener container and automatically register the POJOs as listeners. The full-blown beans definition follows: ```xml - + - + - + @@ -192,7 +192,7 @@ Each time a message is received, the adapter automatically and transparently per ## Reactive Message Listener Container -Spring Data offers `io.valkey.springframework.data.listener.ReactiveValkeyMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. +Spring Data offers `io.valkey.springframework.data.valkey.listener.ReactiveValkeyMessageListenerContainer` which does all the heavy lifting of conversion and subscription state management on behalf of the user. The message listener container itself does not require external threading resources. It uses the driver threads to publish messages. @@ -219,7 +219,7 @@ stream.doOnNext(inner -> // notification hook when Valkey subscriptions are sync ### Subscribing via template API -As mentioned above you can directly use `io.valkey.springframework.data.core.ReactiveValkeyTemplate` to subscribe to channels / patterns. This approach +As mentioned above you can directly use `io.valkey.springframework.data.valkey.core.ReactiveValkeyTemplate` to subscribe to channels / patterns. This approach offers a straight forward, though limited solution as you lose the option to add subscriptions after the initial ones. Nevertheless you still can control the message stream via the returned `Flux` using eg. `take(Duration)`. When done reading, on error or cancellation all bound resources are freed again. diff --git a/docs/src/content/docs/valkey/scripting.mdx b/docs/src/content/docs/valkey/scripting.mdx index 48d31f78f..460ecb85e 100644 --- a/docs/src/content/docs/valkey/scripting.mdx +++ b/docs/src/content/docs/valkey/scripting.mdx @@ -7,9 +7,9 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; Valkey versions 2.6 and higher provide support for running Lua scripts through the [eval](https://valkey.io/commands/eval) and [evalsha](https://valkey.io/commands/evalsha) commands. Spring Data Valkey provides a high-level abstraction for running scripts that handles serialization and automatically uses the Valkey script cache. -Scripts can be run by calling the `execute` methods of `ValkeyTemplate` and `ReactiveValkeyTemplate`. Both use a configurable `io.valkey.springframework.data.core.script.ScriptExecutor` (or `io.valkey.springframework.data.core.script.ReactiveScriptExecutor`) to run the provided script. By default, the `io.valkey.springframework.data.core.script.ScriptExecutor` (or `io.valkey.springframework.data.core.script.ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. +Scripts can be run by calling the `execute` methods of `ValkeyTemplate` and `ReactiveValkeyTemplate`. Both use a configurable `io.valkey.springframework.data.valkey.core.script.ScriptExecutor` (or `io.valkey.springframework.data.valkey.core.script.ReactiveScriptExecutor`) to run the provided script. By default, the `io.valkey.springframework.data.valkey.core.script.ScriptExecutor` (or `io.valkey.springframework.data.valkey.core.script.ReactiveScriptExecutor`) takes care of serializing the provided keys and arguments and deserializing the script result. This is done through the key and value serializers of the template. There is an additional overload that lets you pass custom serializers for the script arguments and the result. -The default `io.valkey.springframework.data.core.script.ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Valkey script cache. +The default `io.valkey.springframework.data.valkey.core.script.ScriptExecutor` optimizes performance by retrieving the SHA1 of the script and attempting first to run `evalsha`, falling back to `eval` if the script is not yet present in the Valkey script cache. The following example runs a common "check-and-set" scenario by using a Lua script. This is an ideal use case for a Valkey script, as it requires that running a set of commands atomically, and the behavior of one command is influenced by the result of another. @@ -71,12 +71,12 @@ end return false ``` -The preceding code configures a `io.valkey.springframework.data.core.script.ValkeyScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). +The preceding code configures a `io.valkey.springframework.data.valkey.core.script.ValkeyScript` pointing to a file called `checkandset.lua`, which is expected to return a boolean value. The script `resultType` should be one of `Long`, `Boolean`, `List`, or a deserialized value type. It can also be `null` if the script returns a throw-away status (specifically, `OK`). :::tip It is ideal to configure a single instance of `DefaultValkeyScript` in your application context to avoid re-calculation of the script's SHA1 on every script run. ::: -The `checkAndSet` method above then runs the scripts. Scripts can be run within a `io.valkey.springframework.data.core.SessionCallback` as part of a transaction or pipeline. See "[Valkey Transactions](/valkey/transactions)" and "[Pipelining](/valkey/pipelining)" for more information. +The `checkAndSet` method above then runs the scripts. Scripts can be run within a `io.valkey.springframework.data.valkey.core.SessionCallback` as part of a transaction or pipeline. See "[Valkey Transactions](/valkey/transactions)" and "[Pipelining](/valkey/pipelining)" for more information. The scripting support provided by Spring Data Valkey also lets you schedule Valkey scripts for periodic running by using the Spring Task and Scheduler abstractions. See the [Spring Framework](https://spring.io/projects/spring-framework/) documentation for more details. diff --git a/docs/src/content/docs/valkey/support-classes.mdx b/docs/src/content/docs/valkey/support-classes.mdx index 5e3471d64..82c8e1fa5 100644 --- a/docs/src/content/docs/valkey/support-classes.mdx +++ b/docs/src/content/docs/valkey/support-classes.mdx @@ -5,16 +5,16 @@ description: Support Classes documentation import { Tabs, TabItem } from '@astrojs/starlight/components'; -Package `io.valkey.springframework.data.support` offers various reusable components that rely on Valkey as a backing store. +Package `io.valkey.springframework.data.valkey.support` offers various reusable components that rely on Valkey as a backing store. Currently, the package contains various JDK-based interface implementations on top of Valkey, such as [atomic](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/atomic/package-summary.html) counters and JDK [Collections](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collection.html). :::note -`io.valkey.springframework.data.support.collections.ValkeyList` is forward-compatible with Java 21 `SequencedCollection`. +`io.valkey.springframework.data.valkey.support.collections.ValkeyList` is forward-compatible with Java 21 `SequencedCollection`. ::: The atomic counters make it easy to wrap Valkey key incrementation while the collections allow easy management of Valkey keys with minimal storage exposure or API leakage. -In particular, the `io.valkey.springframework.data.support.collections.ValkeySet` and `io.valkey.springframework.data.support.collections.ValkeyZSet` interfaces offer easy access to the set operations supported by Valkey, such as `intersection` and `union`. `io.valkey.springframework.data.support.collections.ValkeyList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Valkey, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. -The following example shows the configuration for a bean that uses a `io.valkey.springframework.data.support.collections.ValkeyList`: +In particular, the `io.valkey.springframework.data.valkey.support.collections.ValkeySet` and `io.valkey.springframework.data.valkey.support.collections.ValkeyZSet` interfaces offer easy access to the set operations supported by Valkey, such as `intersection` and `union`. `io.valkey.springframework.data.valkey.support.collections.ValkeyList` implements the `List`, `Queue`, and `Deque` contracts (and their equivalent blocking siblings) on top of Valkey, exposing the storage as a FIFO (First-In-First-Out), LIFO (Last-In-First-Out) or capped collection with minimal configuration. +The following example shows the configuration for a bean that uses a `io.valkey.springframework.data.valkey.support.collections.ValkeyList`: @@ -42,7 +42,7 @@ class MyConfig { xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/docs/src/content/docs/valkey/template.mdx b/docs/src/content/docs/valkey/template.mdx index 8d5748188..90c6fa8fb 100644 --- a/docs/src/content/docs/valkey/template.mdx +++ b/docs/src/content/docs/valkey/template.mdx @@ -5,12 +5,12 @@ description: Template documentation import { Tabs, TabItem } from '@astrojs/starlight/components'; -Most users are likely to use `io.valkey.springframework.data.core.ValkeyTemplate` and its corresponding package, `io.valkey.springframework.data.core` or its reactive variant `io.valkey.springframework.data.core.ReactiveValkeyTemplate`. +Most users are likely to use `io.valkey.springframework.data.valkey.core.ValkeyTemplate` and its corresponding package, `io.valkey.springframework.data.valkey.core` or its reactive variant `io.valkey.springframework.data.valkey.core.ReactiveValkeyTemplate`. The template is, in fact, the central class of the Valkey module, due to its rich feature set. The template offers a high-level abstraction for Valkey interactions. While `[Reactive]ValkeyConnection` offers low-level methods that accept and return binary values (`byte` arrays), the template takes care of serialization and connection management, freeing the user from dealing with such details. -The `io.valkey.springframework.data.core.ValkeyTemplate` class implements the `io.valkey.springframework.data.core.ValkeyOperations` interface and its reactive variant `io.valkey.springframework.data.core.ReactiveValkeyTemplate` implements `io.valkey.springframework.data.core.ReactiveValkeyOperations`. +The `io.valkey.springframework.data.valkey.core.ValkeyTemplate` class implements the `io.valkey.springframework.data.valkey.core.ValkeyOperations` interface and its reactive variant `io.valkey.springframework.data.valkey.core.ReactiveValkeyTemplate` implements `io.valkey.springframework.data.valkey.core.ReactiveValkeyOperations`. :::note The preferred way to reference operations on a `[Reactive]ValkeyTemplate` instance is through the @@ -32,21 +32,21 @@ Moreover, the template provides operations views (following the grouping from th | Interface | Description | |-----------|-------------| | *Key Type Operations* | | -| `io.valkey.springframework.data.core.GeoOperations` | Valkey geospatial operations, such as `GEOADD`, `GEORADIUS`,... | -| `io.valkey.springframework.data.core.HashOperations` | Valkey hash operations | -| `io.valkey.springframework.data.core.HyperLogLogOperations` | Valkey HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... | -| `io.valkey.springframework.data.core.ListOperations` | Valkey list operations | -| `io.valkey.springframework.data.core.SetOperations` | Valkey set operations | -| `io.valkey.springframework.data.core.ValueOperations` | Valkey string (or value) operations | -| `io.valkey.springframework.data.core.ZSetOperations` | Valkey zset (or sorted set) operations | +| `io.valkey.springframework.data.valkey.core.GeoOperations` | Valkey geospatial operations, such as `GEOADD`, `GEORADIUS`,... | +| `io.valkey.springframework.data.valkey.core.HashOperations` | Valkey hash operations | +| `io.valkey.springframework.data.valkey.core.HyperLogLogOperations` | Valkey HyperLogLog operations, such as `PFADD`, `PFCOUNT`,... | +| `io.valkey.springframework.data.valkey.core.ListOperations` | Valkey list operations | +| `io.valkey.springframework.data.valkey.core.SetOperations` | Valkey set operations | +| `io.valkey.springframework.data.valkey.core.ValueOperations` | Valkey string (or value) operations | +| `io.valkey.springframework.data.valkey.core.ZSetOperations` | Valkey zset (or sorted set) operations | | *Key Bound Operations* | | -| `io.valkey.springframework.data.core.BoundGeoOperations` | Valkey key bound geospatial operations | -| `io.valkey.springframework.data.core.BoundHashOperations` | Valkey hash key bound operations | -| `io.valkey.springframework.data.core.BoundKeyOperations` | Valkey key bound operations | -| `io.valkey.springframework.data.core.BoundListOperations` | Valkey list key bound operations | -| `io.valkey.springframework.data.core.BoundSetOperations` | Valkey set key bound operations | -| `io.valkey.springframework.data.core.BoundValueOperations` | Valkey string (or value) key bound operations | -| `io.valkey.springframework.data.core.BoundZSetOperations` | Valkey zset (or sorted set) key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundGeoOperations` | Valkey key bound geospatial operations | +| `io.valkey.springframework.data.valkey.core.BoundHashOperations` | Valkey hash key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundKeyOperations` | Valkey key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundListOperations` | Valkey list key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundSetOperations` | Valkey set key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundValueOperations` | Valkey string (or value) key bound operations | +| `io.valkey.springframework.data.valkey.core.BoundZSetOperations` | Valkey zset (or sorted set) key bound operations | @@ -54,13 +54,13 @@ Moreover, the template provides operations views (following the grouping from th | Interface | Description | |-----------|-------------| | *Key Type Operations* | | -| `io.valkey.springframework.data.core.ReactiveGeoOperations` | Valkey geospatial operations such as `GEOADD`, `GEORADIUS`, and others | -| `io.valkey.springframework.data.core.ReactiveHashOperations` | Valkey hash operations | -| `io.valkey.springframework.data.core.ReactiveHyperLogLogOperations` | Valkey HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) | -| `io.valkey.springframework.data.core.ReactiveListOperations` | Valkey list operations | -| `io.valkey.springframework.data.core.ReactiveSetOperations` | Valkey set operations | -| `io.valkey.springframework.data.core.ReactiveValueOperations` | Valkey string (or value) operations | -| `io.valkey.springframework.data.core.ReactiveZSetOperations` | Valkey zset (or sorted set) operations | +| `io.valkey.springframework.data.valkey.core.ReactiveGeoOperations` | Valkey geospatial operations such as `GEOADD`, `GEORADIUS`, and others | +| `io.valkey.springframework.data.valkey.core.ReactiveHashOperations` | Valkey hash operations | +| `io.valkey.springframework.data.valkey.core.ReactiveHyperLogLogOperations` | Valkey HyperLogLog operations such as (`PFADD`, `PFCOUNT`, and others) | +| `io.valkey.springframework.data.valkey.core.ReactiveListOperations` | Valkey list operations | +| `io.valkey.springframework.data.valkey.core.ReactiveSetOperations` | Valkey set operations | +| `io.valkey.springframework.data.valkey.core.ReactiveValueOperations` | Valkey string (or value) operations | +| `io.valkey.springframework.data.valkey.core.ReactiveZSetOperations` | Valkey zset (or sorted set) operations | @@ -72,7 +72,7 @@ Once configured, the template is thread-safe and can be reused across multiple i `ValkeyTemplate` uses a Java-based serializer for most of its operations. This means that any object written or read by the template is serialized and deserialized through Java. -You can change the serialization mechanism on the template, and the Valkey module offers several implementations, which are available in the `io.valkey.springframework.data.serializer` package. +You can change the serialization mechanism on the template, and the Valkey module offers several implementations, which are available in the `io.valkey.springframework.data.valkey.serializer` package. See [Serializers](#serializers) for more information. You can also set any of the serializers to null and use ValkeyTemplate with raw byte arrays by setting the `enableDefaultSerializer` property to `false`. Note that the template requires all keys to be non-null. @@ -135,9 +135,9 @@ class MyConfig { xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + ... @@ -244,9 +244,9 @@ class ValkeyConfiguration { xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + ``` @@ -311,27 +311,27 @@ From the framework perspective, the data stored in Valkey is only bytes. While Valkey itself supports various types, for the most part, these refer to the way the data is stored rather than what it represents. It is up to the user to decide whether the information gets translated into strings or any other objects. -In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Valkey in the `io.valkey.springframework.data.serializer` package. +In Spring Data, the conversion between the user (custom) types and raw data (and vice-versa) is handled by Spring Data Valkey in the `io.valkey.springframework.data.valkey.serializer` package. This package contains two types of serializers that, as the name implies, take care of the serialization process: -* Two-way serializers based on `io.valkey.springframework.data.serializer.ValkeySerializer`. +* Two-way serializers based on `io.valkey.springframework.data.valkey.serializer.ValkeySerializer`. * Element readers and writers that use `ValkeyElementReader` and ``ValkeyElementWriter``. The main difference between these variants is that `ValkeySerializer` primarily serializes to `byte[]` while readers and writers use `ByteBuffer`. Multiple implementations are available (including two that have been already mentioned in this documentation): -* `io.valkey.springframework.data.serializer.JdkSerializationValkeySerializer`, which is used by default for `io.valkey.springframework.data.cache.ValkeyCache` and `io.valkey.springframework.data.core.ValkeyTemplate`. +* `io.valkey.springframework.data.valkey.serializer.JdkSerializationValkeySerializer`, which is used by default for `io.valkey.springframework.data.valkey.cache.ValkeyCache` and `io.valkey.springframework.data.valkey.core.ValkeyTemplate`. * the `StringValkeySerializer`. -However, one can use `OxmSerializer` for Object/XML mapping through Spring [OXM](https://docs.spring.io/spring-framework/reference/data-access.html#oxm) support or `io.valkey.springframework.data.serializer.Jackson2JsonValkeySerializer` or `io.valkey.springframework.data.serializer.GenericJackson2JsonValkeySerializer` for storing data in [JSON](https://en.wikipedia.org/wiki/JSON) format. +However, one can use `OxmSerializer` for Object/XML mapping through Spring [OXM](https://docs.spring.io/spring-framework/reference/data-access.html#oxm) support or `io.valkey.springframework.data.valkey.serializer.Jackson2JsonValkeySerializer` or `io.valkey.springframework.data.valkey.serializer.GenericJackson2JsonValkeySerializer` for storing data in [JSON](https://en.wikipedia.org/wiki/JSON) format. Do note that the storage format is not limited only to values. It can be used for keys, values, or hashes without any restrictions. :::danger -By default, `io.valkey.springframework.data.cache.ValkeyCache` and `io.valkey.springframework.data.core.ValkeyTemplate` are configured to use Java native serialization. +By default, `io.valkey.springframework.data.valkey.cache.ValkeyCache` and `io.valkey.springframework.data.valkey.core.ValkeyTemplate` are configured to use Java native serialization. Java native serialization is known for allowing the running of remote code caused by payloads that exploit vulnerable libraries and classes injecting unverified bytecode. Manipulated input could lead to unwanted code being run in the application during the deserialization step. As a consequence, do not use serialization in untrusted environments. diff --git a/docs/src/content/docs/valkey/transactions.md b/docs/src/content/docs/valkey/transactions.md index 02101ab37..14ec2d67a 100644 --- a/docs/src/content/docs/valkey/transactions.md +++ b/docs/src/content/docs/valkey/transactions.md @@ -4,10 +4,10 @@ description: Transactions documentation --- Valkey provides support for [transactions](https://valkey.io/topics/transactions) through the `multi`, `exec`, and `discard` commands. -These operations are available on `io.valkey.springframework.data.core.ValkeyTemplate`. +These operations are available on `io.valkey.springframework.data.valkey.core.ValkeyTemplate`. However, `ValkeyTemplate` is not guaranteed to run all the operations in the transaction with the same connection. -Spring Data Valkey provides the `io.valkey.springframework.data.core.SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Valkey transactions.The following example uses the `multi` method: +Spring Data Valkey provides the `io.valkey.springframework.data.valkey.core.SessionCallback` interface for use when multiple operations need to be performed with the same `connection`, such as when using Valkey transactions.The following example uses the `multi` method: ```java //execute a transaction diff --git a/docs/src/content/docs/valkey/valkey-cache.md b/docs/src/content/docs/valkey/valkey-cache.md index c76a5e6b1..1dd9e9c22 100644 --- a/docs/src/content/docs/valkey/valkey-cache.md +++ b/docs/src/content/docs/valkey/valkey-cache.md @@ -3,8 +3,8 @@ title: Valkey Cache description: Valkey Cache documentation --- -Spring Data Valkey provides an implementation of Spring Framework's [Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration.html#cache) in the `io.valkey.springframework.data.cache` package. -To use Valkey as a backing implementation, add `io.valkey.springframework.data.cache.ValkeyCacheManager` to your configuration, as follows: +Spring Data Valkey provides an implementation of Spring Framework's [Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration.html#cache) in the `io.valkey.springframework.data.valkey.cache` package. +To use Valkey as a backing implementation, add `io.valkey.springframework.data.valkey.cache.ValkeyCacheManager` to your configuration, as follows: ```java @Bean @@ -13,7 +13,7 @@ public ValkeyCacheManager cacheManager(ValkeyConnectionFactory connectionFactory } ``` -`ValkeyCacheManager` behavior can be configured with `io.valkey.springframework.data.cache.ValkeyCacheManager$ValkeyCacheManagerBuilder`, letting you set the default `io.valkey.springframework.data.cache.ValkeyCacheManager`, transaction behavior, and predefined caches. +`ValkeyCacheManager` behavior can be configured with `io.valkey.springframework.data.valkey.cache.ValkeyCacheManager$ValkeyCacheManagerBuilder`, letting you set the default `io.valkey.springframework.data.valkey.cache.ValkeyCacheManager`, transaction behavior, and predefined caches. ```java ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(connectionFactory) @@ -26,7 +26,7 @@ ValkeyCacheManager cacheManager = ValkeyCacheManager.builder(connectionFactory) As shown in the preceding example, `ValkeyCacheManager` allows custom configuration on a per-cache basis. -The behavior of `io.valkey.springframework.data.cache.ValkeyCache` created by `io.valkey.springframework.data.cache.ValkeyCacheManager` is defined with `ValkeyCacheConfiguration`. +The behavior of `io.valkey.springframework.data.valkey.cache.ValkeyCache` created by `io.valkey.springframework.data.valkey.cache.ValkeyCacheManager` is defined with `ValkeyCacheConfiguration`. The configuration lets you set key expiration times, prefixes, and `ValkeySerializer` implementations for converting to and from the binary storage format, as shown in the following example: ```java @@ -35,7 +35,7 @@ ValkeyCacheConfiguration cacheConfiguration = ValkeyCacheConfiguration.defaultCa .disableCachingNullValues(); ``` -`io.valkey.springframework.data.cache.ValkeyCacheManager` defaults to a lock-free `io.valkey.springframework.data.cache.ValkeyCacheWriter` for reading and writing binary values. +`io.valkey.springframework.data.valkey.cache.ValkeyCacheManager` defaults to a lock-free `io.valkey.springframework.data.valkey.cache.ValkeyCacheWriter` for reading and writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non-atomic commands for the `Cache` `putIfAbsent` and `clean` operations, as those require multiple commands to be sent to Valkey. The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times. diff --git a/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md index db2fed7fe..31aec1b36 100644 --- a/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md +++ b/docs/src/content/docs/valkey/valkey-repositories/cdi-integration.md @@ -8,7 +8,7 @@ Spring offers sophisticated support for creating bean instances. Spring Data Valkey ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. The extension is part of the JAR, so, to activate it, drop the Spring Data Valkey JAR into your classpath. -You can then set up the infrastructure by implementing a CDI Producer for the `io.valkey.springframework.data.connection.ValkeyConnectionFactory` and `io.valkey.springframework.data.core.ValkeyOperations`, as shown in the following example: +You can then set up the infrastructure by implementing a CDI Producer for the `io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory` and `io.valkey.springframework.data.valkey.core.ValkeyOperations`, as shown in the following example: ```java class ValkeyOperationsProducer { @@ -60,6 +60,6 @@ class RepositoryClient { } ``` -A Valkey Repository requires `io.valkey.springframework.data.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.core.ValkeyKeyValueTemplate` instances. +A Valkey Repository requires `io.valkey.springframework.data.valkey.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.valkey.core.ValkeyKeyValueTemplate` instances. These beans are created and managed by the Spring Data CDI extension if no provided beans are found. -You can, however, supply your own beans to configure the specific properties of `io.valkey.springframework.data.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.core.ValkeyKeyValueTemplate`. +You can, however, supply your own beans to configure the specific properties of `io.valkey.springframework.data.valkey.core.ValkeyKeyValueAdapter` and `io.valkey.springframework.data.valkey.core.ValkeyKeyValueTemplate`. diff --git a/docs/src/content/docs/valkey/valkey-repositories/expirations.md b/docs/src/content/docs/valkey/valkey-repositories/expirations.md index 6e086add9..ddbef4fb0 100644 --- a/docs/src/content/docs/valkey/valkey-repositories/expirations.md +++ b/docs/src/content/docs/valkey/valkey-repositories/expirations.md @@ -5,7 +5,7 @@ description: Expirations documentation Objects stored in Valkey may be valid only for a certain amount of time. This is especially useful for persisting short-lived objects in Valkey without having to remove them manually when they reach their end of life. -The expiration time in seconds can be set with `@ValkeyHash(timeToLive=...)` as well as by using `io.valkey.springframework.data.core.convert.KeyspaceConfiguration$KeyspaceSettings` (see [Keyspaces](/valkey/valkey-repositories/keyspaces)). +The expiration time in seconds can be set with `@ValkeyHash(timeToLive=...)` as well as by using `io.valkey.springframework.data.valkey.core.convert.KeyspaceConfiguration$KeyspaceSettings` (see [Keyspaces](/valkey/valkey-repositories/keyspaces)). More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or a method. However, do not apply `@TimeToLive` on both a method and a property within the same class. @@ -39,16 +39,16 @@ public class TimeToLiveOnMethod { Annotating a property explicitly with `@TimeToLive` reads back the actual `TTL` or `PTTL` value from Valkey. -1 indicates that the object has no associated expiration. ::: -The repository implementation ensures subscription to [Valkey keyspace notifications](https://valkey.io/topics/notifications) via `io.valkey.springframework.data.listener.ValkeyMessageListenerContainer`. +The repository implementation ensures subscription to [Valkey keyspace notifications](https://valkey.io/topics/notifications) via `io.valkey.springframework.data.valkey.listener.ValkeyMessageListenerContainer`. When the expiration is set to a positive value, the corresponding `EXPIRE` command is run. In addition to persisting the original, a phantom copy is persisted in Valkey and set to expire five minutes after the original one. -This is done to enable the Repository support to publish `io.valkey.springframework.data.core.ValkeyKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. +This is done to enable the Repository support to publish `io.valkey.springframework.data.valkey.core.ValkeyKeyExpiredEvent`, holding the expired value in Spring's `ApplicationEventPublisher` whenever a key expires, even though the original values have already been removed. Expiry events are received on all connected applications that use Spring Data Valkey repositories. By default, the key expiry listener is disabled when initializing the application. The startup mode can be adjusted in `@EnableValkeyRepositories` or `ValkeyKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. -See `io.valkey.springframework.data.core.ValkeyKeyValueAdapter$EnableKeyspaceEvents` for possible values. +See `io.valkey.springframework.data.valkey.core.ValkeyKeyValueAdapter$EnableKeyspaceEvents` for possible values. The `ValkeyKeyExpiredEvent` holds a copy of the expired domain object as well as the key. diff --git a/docs/src/content/docs/valkey/valkey-streams.md b/docs/src/content/docs/valkey/valkey-streams.md index 89097390c..2c26e25f5 100644 --- a/docs/src/content/docs/valkey/valkey-streams.md +++ b/docs/src/content/docs/valkey/valkey-streams.md @@ -18,7 +18,7 @@ Although this pattern has similarities to [Pub/Sub](/valkey/pubsub), the main di While Pub/Sub relies on the broadcasting of transient messages (i.e. if you don't listen, you miss a message), Valkey Stream use a persistent, append-only data type that retains messages until the stream is trimmed. Another difference in consumption is that Pub/Sub registers a server-side subscription. Valkey pushes arriving messages to the client while Valkey Streams require active polling. -The `io.valkey.springframework.data.connection` and `io.valkey.springframework.data.stream` packages provide the core functionality for Valkey Streams. +The `io.valkey.springframework.data.valkey.connection` and `io.valkey.springframework.data.valkey.stream` packages provide the core functionality for Valkey Streams. ## Appending @@ -73,8 +73,8 @@ Due to its blocking nature, low-level polling is not attractive, as it requires Spring Data ships with two implementations tailored to the used programming model: -* `io.valkey.springframework.data.stream.StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Valkey Stream and drive the `io.valkey.springframework.data.stream.StreamListener` instances that are injected into it. -* `io.valkey.springframework.data.stream.StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Valkey Stream as potentially infinite stream and emit stream messages through a `Flux`. +* `io.valkey.springframework.data.valkey.stream.StreamMessageListenerContainer` acts as message listener container for imperative programming models. It is used to consume records from a Valkey Stream and drive the `io.valkey.springframework.data.valkey.stream.StreamListener` instances that are injected into it. +* `io.valkey.springframework.data.valkey.stream.StreamReceiver` provides a reactive variant of a message listener. It is used to consume messages from a Valkey Stream as potentially infinite stream and emit stream messages through a `Flux`. `StreamMessageListenerContainer` and `StreamReceiver` are responsible for all threading of message reception and dispatch into the listener for processing. A message listener container/receiver is the intermediary between an MDP and a messaging provider and takes care of registering to receive messages, resource acquisition and release, exception conversion, and the like. This lets you as an application developer write the (possibly complex) business logic associated with receiving a message (and reacting to it) and delegates boilerplate Valkey infrastructure concerns to the framework. @@ -82,7 +82,7 @@ Both containers allow runtime configuration changes so that you can add or remov #### Imperative `StreamMessageListenerContainer` -In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `io.valkey.springframework.data.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. +In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Stream-Driven POJO (SDP) acts as a receiver for Stream messages. The one restriction on an SDP is that it must implement the `io.valkey.springframework.data.valkey.stream.StreamListener` interface. Please also be aware that in the case where your POJO receives messages on multiple threads, it is important to ensure that your implementation is thread-safe. ```java class ExampleStreamListener implements StreamListener> {