| Metadata | Value |
|---|---|
| Date | 2025-08-06 |
| Author | @piotrpio |
| Status | Proposed |
| Tags | jetstream, client, spec, orbit, kv, refinement |
| Updates | ADR-8 |
| Revision | Date | Author | Info |
|---|---|---|---|
| 1 | 2025-08-06 | @piotrpio | Initial design |
JetStream Key-Value stores require flexible data transformation capabilities to handle various encoding scenarios such as character escaping, path notation conversion, encryption, and custom transformations. Currently, these transformations must be implemented at the application level, leading to inconsistent implementations and increased complexity.
A standardized codec system would provide transparent encoding and decoding of keys and values while maintaining full compatibility with the existing KV API, enabling users to handle special characters, implement security features, and perform custom transformations seamlessly.
The JetStream Key-Value store uses NATS subjects as keys and message payloads as values. Several use cases require transformation of these elements:
- Character Escaping: Keys containing special characters (spaces, dots, etc.) that are invalid in NATS subjects
- Path Translation: Converting between different path notation styles (e.g., filesystem paths to NATS subjects)
- Security: End-to-end encryption
- Custom Transformations: Application-specific encoding requirements
The codec system is based on two separate interfaces for transforming keys and values:
// KeyCodec transforms keys before storage and after retrieval
type KeyCodec interface {
Encode(key string) string
Decode(encoded string) string
}
// ValueCodec transforms values before storage and after retrieval
type ValueCodec interface {
Encode(value []byte) []byte
Decode(encoded []byte) []byte
}A codec-enabled KV bucket wraps the standard KV interface. It should be possible to use codecs for both keys and values independently, allowing for flexible transformations.
type CodecKV struct {
kv jetstream.KeyValue
keyCodec KeyCodec
valueCodec ValueCodec
}
// Constructor functions
func New(kv jetstream.KeyValue, keyCodec KeyCodec, valueCodec ValueCodec) jetstream.KeyValue
// NewForKey creates a KV bucket with a key codec only, using no-op value codec.
func NewForKey(kv jetstream.KeyValue, keyCodec KeyCodec) jetstream.KeyValue
// NewForValue creates a KV bucket with a value codec only, using no-op key codec.
func NewForValue(kv jetstream.KeyValue, valueCodec ValueCodec) jetstream.KeyValueAll KV operations work transparently with codecs:
// Standard KV operations
err := codecKV.Put(ctx, key, value)
entry, err := codecKV.Get(ctx, key)
watcher, err := codecKV.Watch(ctx, pattern)
keys, err := codecKV.Keys(ctx)
// History and other operations
history, err := codecKV.History(ctx, key)
err := codecKV.Delete(ctx, key)
err := codecKV.Purge(ctx, key)Users can implement custom codecs for specific requirements:
type AESCodec struct {
cipher cipher.Block
}
func (c *AESCodec) Encode(value []byte) []byte {
// Implement AES encryption
return encrypted
}
func (c *AESCodec) Decode(value []byte) []byte {
// Implement AES decryption
return decrypted
}
// Usage
aesCodec := &AESCodec{cipher: aesCipher}
codecKV := kvcodec.NewForValue(kv, aesCodec)Key codecs should optionally support wildcard patterns for watching and listing keys. The KeyCodec interface can be extended to handle wildcard patterns, allowing users to specify how wildcards should be encoded and decoded.
Special handling of wildcards should be optional when implementing custom codecs, in a language idiomatic way.
type FilterableKeyCodec interface {
KeyCodec
EncodeFilter(filter string) (string, error)
}
// Example implementation
func (c *CustomCodec) EncodeFilter(filter string) (string, error) {
// Handle wildcard patterns specially to preserve filtering
return encodePreservingWildcards(filter), nil
}It should be possible to chain multiple codecs together, allowing for complex transformations. Keys or values should be processed through each codec in sequence and decoded in reverse order.
keyChain, _ := kvcodec.NewKeyChainCodec(pathCodec, base64Codec)
valueChain, _ := kvcodec.NewValueChainCodec(aesCodec, base64Codec)
codecKV := kvcodec.New(kv, keyChain, valueChain)Passes data through unchanged, useful for selective encoding:
type NoOpCodec struct{}
func (c NoOpCodec) Encode(s string) string { return s }
func (c NoOpCodec) Decode(s string) string { return s }Encodes keys/values using url-encoded base64:
type Base64Codec struct{}
// Example usage:
codec := kvcodec.Base64Codec()
codecKV := kvcodec.New(kv, codec, kvcodec.NoOpCodec())
// "Acme Inc.contact" becomes "QWNtZSBJbmMuY29udGFjdA=="
codecKV.Put(ctx, "Acme Inc.contact", []byte("info@acme.com"))Translates between path-style and NATS-style keys:
type PathCodec struct {
separator string
}
// Example: converts "user/profile/settings" to "user.profile.settings"
codec := kvcodec.NewPathCodec("/")
codecKV := kvcodec.NewForKey(kv, codec)As encoding leading and trailing slashes cannot be preserved directly as leading and trailing dots, the codec should handle these cases by encoding leading slashes as _root_ and trimming trailing slashes.