Key-value store
t_key_value is a small shared store for values that do not deserve a table. A value lives under a
group and a key, and can expire.
Use it for a last-sync time, a cached API answer, a one-at-a-time lock or a flag.
| It is | It is not |
|---|---|
| A place for small JSON values | A cache. Reads hit the database, and nothing is kept in memory. |
Safe for one-holder locks (osc_kv_claim()) |
A queue. Use background jobs. |
| Cleaned up for you when a key expires | A place for personal data. See Personal data. |
The shortest complete example
Section titled “The shortest complete example”osc_kv_set('acme', 'last_sync', array('at' => time()), 3600); // lives one hour$sync = osc_kv_get('acme', 'last_sync', array()); // array() once it expired
if (osc_kv_claim('acme', 'nightly_export', 600)) { // one process at a time acme_export(); osc_kv_delete('acme', 'nightly_export'); // release early}Functions
Section titled “Functions”All are in oc-includes/osclass/helpers/hKv.php. Each throws InvalidArgumentException for
a bad group or key, and mindstellar\database\DbException when the query fails.
| Function | Returns |
|---|---|
osc_kv_get(string $group, string $key, $default = null) |
The stored value, decoded. $default when the key is absent or expired. |
osc_kv_set(string $group, string $key, $value, ?int $ttl = null): void |
Nothing. Replaces any value and expiry the key had. $ttl is seconds; null keeps it until deleted. |
osc_kv_delete(string $group, string $key): bool |
false when there was nothing to remove. |
osc_kv_claim(string $group, string $key, int $ttl, string $state = 'locked'): bool |
true for exactly one caller while the key is absent or expired. |
osc_kv_list(string $group, int $limit = 100, ?string $updatedBefore = null, ?string $afterKey = null): array |
One page (at most 1000) of live keys in key order, as {key, value, updated_at, expires_at}. Pass the last key as $afterKey for the next page. $updatedBefore is a UTC Y-m-d H:i:s; a key never rewritten counts from its creation. |
osc_kv_delete_group(string $group): int |
How many keys were removed. |
Values
Section titled “Values”Values are stored as JSON and come back decoded. Strings, numbers, booleans, null and
arrays all round-trip. An object comes back as an associative array.
osc_kv_get() tells a missing key from a stored null:
| State of the key | osc_kv_get('acme', 'k', 'fallback') |
|---|---|
| Never set, deleted, or expired | 'fallback' |
Set to null |
null |
osc_kv_set() throws when the value cannot be encoded as JSON, or when the JSON is over
16 MB (16,777,215 bytes). Keep values small anyway: every read loads the whole value.
Group and key rules
Section titled “Group and key rules”| Part | Rule |
|---|---|
| Group | 1 to 64 characters of a-z, 0-9, _, ., -. Starts with a letter or digit. The same as ^[a-z0-9][a-z0-9_.-]{0,63}$. |
| Key | 1 to 191 characters of valid UTF-8. No control characters. No space at the start or end. |
Keys match exactly, byte for byte: Order and order are two keys.
Name your group after your plugin slug, such as acme. Do not use a name core uses:
| Reserved group | Used by |
|---|---|
api_idempotency |
The REST API’s Idempotency-Key replay |
api_webhook |
The REST API’s webhook endpoints: one key per endpoint (ep_…), holding its address, events, secret and failure count. It has no expiry. |
market |
The plugin and theme catalogue cache from the market, so it is not loaded on every page. |
alerts_sent |
How many alert e-mails went out, one key per day. |
cron |
When the hourly, daily and weekly schedules last ran and next run. |
email_change |
A pending e-mail change, one key per user. It expires after 7 days. |
plugin_categories |
The categories each plugin is limited to, one key per plugin. |
upload.… |
Photos uploaded before their listing is saved: one group per form or API user, expiring after 2 hours. Every group that starts with upload. is reserved. |
Expiry
Section titled “Expiry”Pass $ttl to osc_kv_set() and the key expires that many seconds later. A $ttl below 1
is an error.
- An expired key reads as absent straight away.
- The daily cron removes the expired rows, so they do not pile up.
osc_kv_set()on an existing key replaces its value and its expiry. A call with no$ttlmakes it permanent.
Locks with osc_kv_claim()
Section titled “Locks with osc_kv_claim()”osc_kv_claim() takes a key for $ttl seconds. Of several callers racing for it, exactly
one gets true. The rest get false until the key is deleted or expires.
if (!osc_kv_claim('acme', 'import', 900)) { return; // another run holds it}try { acme_import();} finally { osc_kv_delete('acme', 'import');}Always pass a realistic $ttl. If the holder crashes, the lock frees itself when it
expires. $state is a short label stored with the key: 1 to 16 characters of a-z, 0-9,
_, ., -.
Personal data
Section titled “Personal data”t_key_value has no user column. Core’s data export and account erasure reach only core’s own
email_change group. If your plugin stores something about a person here, add it to the export
with the personal_data_export filter and erase it on the delete_user action:
osc_add_filter('personal_data_export', static function (array $out, $userId): array { $out['data']['acme_notes'] = [ 'retention' => 'deleted', 'why' => 'Notes the user saved.', 'rows' => acme_notes_for((int) $userId), ];
return $out;});osc_add_hook('delete_user', static fn ($userId) => acme_delete_notes((int) $userId));Name the key <plugin>_<name>. Core does not redact these rows, so leave out secrets and tokens.
Clean up on uninstall
Section titled “Clean up on uninstall”Remove your whole group when the plugin is uninstalled:
osc_add_hook(osc_plugin_path(__FILE__) . '_uninstall', static function () { osc_kv_delete_group('acme');});