Skip to content

26 enhanced kv store key value experience#80

Open
corrideat wants to merge 36 commits into
mainfrom
26-enhanced-kv-store-key-value-experience
Open

26 enhanced kv store key value experience#80
corrideat wants to merge 36 commits into
mainfrom
26-enhanced-kv-store-key-value-experience

Conversation

@corrideat

@corrideat corrideat commented May 27, 2026

Copy link
Copy Markdown
Member

Closes #26

KV-REVAMPED.md KV-REVAMPED.md
KV-REVAMPED.md contains the spec used (which this PR should conform to).

AI disclosure

GPT-5.5, Opus 4.7 and GLM 5.1 were used in generating code for this PR. GPT-5.5, Opus 4.7 & GLM 5.2 were used for reviews.

@corrideat

corrideat commented May 27, 2026

Copy link
Copy Markdown
Member Author

/crush_fast


AI review started.

Comment thread src/chelonia.ts Outdated
Comment thread src/kv.ts Outdated
Comment thread src/kv.ts
Comment on lines +816 to +830
if (!def || typeof def !== 'object') {
throw new ChelErrorKvSlotInvalid('[chelonia/kv] defineSlot: invalid definition')
}
if (typeof def.key !== 'string' || def.key.length === 0) {
throw new ChelErrorKvSlotInvalid('[chelonia/kv] defineSlot: invalid key')
}
const types = Array.isArray(def.contractType) ? def.contractType : [def.contractType]
if (types.length === 0) {
throw new ChelErrorKvSlotInvalid('[chelonia/kv] defineSlot: contractType required')
}
// Runtime validation of optional fields (SBP selectors are callable
// from JavaScript without TypeScript enforcement).
if (def.match != null && typeof def.match !== 'function') {
throw new ChelErrorKvSlotInvalid('[chelonia/kv] defineSlot: match must be a function')
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using Zod for schema validation.

@corrideat

corrideat commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

Testing this, I noticed this for notifications.spec.js (which also seems to be transiently failing):

[19:46:48.781] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:48.828] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:49.521] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",128]
[19:46:49.523] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:49.523] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:49.576] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:50.161] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",128]
[19:46:50.165] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:50.165] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:50.207] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:51.005] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",124]
[19:46:51.007] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:51.008] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:51.048] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:52.006] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",128]
[19:46:52.007] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:52.008] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:52.048] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:52.691] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",128]
[19:46:52.695] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:52.695] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:52.763] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412
[19:46:52.789] DEBUG (62571): [pubsub] Pinging clients
[19:46:53.524] DEBUG (62571): [sbp] backend/server/updateSize ["zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9",124]
[19:46:53.528] DEBUG (62571): [pubsub] Broadcasting KV change on zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9 to key notifications
[19:46:53.528] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 204
[19:46:53.570] DEBUG (62571): 127.0.0.1: POST /kv/zLDXeQ2AgfCuPcNQvBuix2oY8Zc1xH3nKTLeQShRVN1k8wACNT1RB3n9/notifications --> 412

Increasing the FIFO queue size from 8 to 128 seems to fix the failing tests, but we shouldn't be having so many conflicts writing to the notifications queue, and this points to a likely bug in the echo-suppression logic this PR implements. If we indeed have buggy logic, it needs to be fixed (to determine this, we need to examine actual update calls to see what's being written). The logs suggest that the client is confused about what should be written / updated and attempts to repeatedly make the same update.

This PR introduces a nonce in KV writes (which are then put in a FIFO queue) so that when we receive a value over pubsub, we can tell whether it's the value we've just written or an update made by someone else. However, we likely don't need this additional nonce (which is stored in the KV value) since we already hash KV values and use the etag / x-cid headers to expose these hashes to clients. However, we don't send this x-cid / etag over pubsub. Two possible solutions:

  1. Send this information over pubsub (requires changing chel serve)
  2. (less preferred) Have clients compute the x-cid value. This is less preferred because it requires the clients and the server agree on how the x-cid value should be computed (exact algorithm).

This seems resolved now, an it was caused by POST /kv/... not returning the ETag value, which is now in okTurtles/chel#140

@corrideat corrideat marked this pull request as ready for review June 21, 2026 22:40
@corrideat corrideat requested a review from taoeffect June 21, 2026 22:40
Comment thread src/chelonia.ts
Comment on lines +439 to +445
this.kvSlots = new Map()
this.kvSlotsByContractID = new Map()
this.kvActiveFilters = new Map()
this.kvFilterDirty = new Set()
this.kvLocalEchoCIDs = new Map()
this.kvPendingWrites = new Map()
this.defContractKvByManifest = new Map()

@taoeffect taoeffect Jun 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the other variables in this section begin with kv, kinda indicating that they're together as part of the same system, except for this last one, which also has Kv in the name but it's not at the start of the variable name. For consistency, would it make sense to move that to the start of the variable name, e.g. this.kvDefConractByManifest, or is it intended to be set apart from the others?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's different from the others in the sense that it's only used in / by 'chelonia/defineContract', which mostly follows that specific naming convention (this.defContractSelectors, this.defContract).

OTOH, you're right that we do have all of these other keys starting with kv when they are KV-related.

Comment thread src/chelonia.ts
Comment on lines +1181 to +1205
// Share one lazy parsed wrapper between the legacy callback and the
// slot layer. If either consumer forces `.data`, the decoded value
// or thrown error is cached; both consumers can therefore observe
// and log the same decode failure, and the legacy callback may
// decode frames the slot layer would skip as self-echoes.
const parsed = parseEncryptedOrUnencryptedMessage<object>(this, {
contractID: msg.channelID,
meta: msg.key,
serializedData: JSON.parse(Buffer.from(msg.data).toString())
})
if (legacyKvHandler) {
try {
;(
legacyKvHandler as unknown as (
this: PubSubClient,
msg: [string, ParsedEncryptedOrUnencryptedMessage<object>],
) => void
).call(this.pubsub, [msg.key, parsed])
} catch (e) {
console.error(
`[chelonia] legacy kv pubsub callback threw for ${msg.channelID}::${msg.key}`,
e
)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't quite understand what this "legacy layer" thing is. Do we need it or can it be removed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether we 'need' it or not depends on what the API should look like. This is used for supporting the existing way of handling KV updates in Group Income which doesn't use chelonia/kv/update.

If we were to remove it, then application developers won't have easy access to these lower level primitives to handle KV updates directly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Letting developers have access to low-level APIs and doing backwards compatibility when it's not needed are two different things.

The word "legacy" sounds like backwards compatibility. If it's not backwards compat then it should probably be renamed. If it is backwards compat then it should be removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhanced KV-store / Key-value experience

2 participants