Extend SugarDB commands using JavaScript Modules (#161)

Implemented extensibility with JavaScript modules - @kelvinmwinuka
This commit is contained in:
Kelvin Mwinuka
2025-01-12 01:18:21 +08:00
committed by GitHub
parent 1c8b25436a
commit 136d7c61c1
24 changed files with 6446 additions and 1982 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ services:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/standalone_node:/var/lib/sugardb - ./internal/volumes/nodes/standalone_node:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua - ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet
@@ -90,6 +91,8 @@ services:
volumes: volumes:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/cluster_node_1:/var/lib/sugardb - ./internal/volumes/nodes/cluster_node_1:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet
@@ -134,6 +137,8 @@ services:
volumes: volumes:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/cluster_node_2:/var/lib/sugardb - ./internal/volumes/nodes/cluster_node_2:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet
@@ -178,6 +183,8 @@ services:
volumes: volumes:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/cluster_node_3:/var/lib/sugardb - ./internal/volumes/nodes/cluster_node_3:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet
@@ -222,6 +229,8 @@ services:
volumes: volumes:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/cluster_node_4:/var/lib/sugardb - ./internal/volumes/nodes/cluster_node_4:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet
@@ -266,5 +275,7 @@ services:
volumes: volumes:
- ./internal/volumes/config:/etc/sugardb/config - ./internal/volumes/config:/etc/sugardb/config
- ./internal/volumes/nodes/cluster_node_5:/var/lib/sugardb - ./internal/volumes/nodes/cluster_node_5:/var/lib/sugardb
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
- ./internal/volumes/modules/js:/var/lib/sugardb/scripts/js
networks: networks:
- testnet - testnet

548
docs/docs/extension/js.mdx Normal file
View File

@@ -0,0 +1,548 @@
---
title: JavaScript Modules
toc_min_heading_level: 2
toc_max_heading_level: 4
---
import LoadModuleDocs from "@site/src/components/load_module"
import CodeBlock from "@theme/CodeBlock"
# JavaScript Modules
SugarDB allows you to create new command modules using JavaScript.
These scripts are loaded into SugarDB at runtime and can be triggered by both embedded clients and
TCP clients just like native commands.
SugarDB uses the [Otto engine (v0.5.1)](https://github.com/robertkrimen/otto) which targets ES5.
ES6 and later features will not be avaliable so you should refrain from using them.
## Creating a JavaScript Module
A JavaScript module has the following anatomy:
```js
// The keyword to trigger the command
var command = "JS.EXAMPLE"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["generic", "write", "fast"]
// The description of the command.
var description = "(JS.EXAMPLE) Example JS command that sets various data types to keys"
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
if (command.length > 1) {
throw "wrong number of args, expected 0"
}
return {
readKeys: [],
writeKeys: []
}
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Set various data types to keys
var keyValues = {
"numberKey": 42,
"stringKey": "Hello, SugarDB!",
"floatKey": 3.142,
"nilKey": null,
}
// Store the values in the database
setValues(keyValues)
// Verify the values have been set correctly
var keysToGet = ["numberKey", "stringKey", "floatKey", "nilKey"]
var retrievedValues = getValues(keysToGet)
// Create an array to track mismatches
var mismatches = [];
for (var key in keyValues) {
if (Object.prototype.hasOwnProperty.call(keyValues, key)) {
var expectedValue = keyValues[key];
var retrievedValue = retrievedValues[key];
if (retrievedValue !== expectedValue) {
var msg = "Key " + key + ": expected " + expectedValue + ", got " + retrievedValue
mismatches.push(msg);
console.log(msg)
}
}
}
// If mismatches exist, return an error
if (mismatches.length > 0) {
throw "values mismatch"
}
// If all values match, return OK
return "+OK\r\n"
}
```
## Loading JavaScript Modules
<LoadModuleDocs module="js" />
## Standard Data Types
Sugar DB supports the following standard data types in JavaScript modules:
- string
- number (integers and floating-point numbers)
- null
- arrays (tables with integer keys)
These data types can be stored using the setValues function and retrieved using the getValues function.
## Custom Data Types
In addition to the standard data types, SugarDB also supports custom data types in JavaScript modules.
These custom data types include:
- Hashes
- Sets
- Sorted Sets
Just like the standard types, these custom data types can be stored and retrieved using the setValues
and getValues functions respectively.
### Hashes
The hash data type is a custom data type in SugarDB designed for storing and managing key-value pairs.
It supports several methods for interacting with the hash, including adding, updating, retrieving, deleting,
and checking values.This section explains how to make use of the hash data type in your JavaScript modules.
#### Creating a Hash
```js
var myHash = new Hash();
```
#### Hash methods
`set` - Adds or updates key-value pairs in the hash. If the key exists,
the value is updated; otherwise, it is added.
```js
var myHash = new Hash();
var numUpdated = myHash.set({
"field1": "value1",
"field2": "value2",
"field3": "value3",
"field4": "value4"
});
console.log(numUpdated) // Output: 4
```
`setnx` - Adds key-value pairs to the hash only if the key does not already exist.
```js
var myHash = new Hash();
myHash.set({"field1": "value1"});
var numAdded = myHash.setnx({
"field1": "newValue", // Will not overwrite because field1 exists
"field2": "value2" // Will be added
})
console.log(numAdded) // Output: 1
```
`get` - Retrieves the values for the specified keys. Returns nil for keys that do not exist.
```js
var myHash = new Hash();
myHash.set({
key1: "value1" ,
key2: "value2"
});
// Get values from the hash
var values = myHash.get(["key1", "key2", "key3"]);
// Iterate over the values and log them
for (var key in values) {
if (values.hasOwnProperty(key)) {
console.log(key, values[key]); // Output: key1 value1, key2 value2, key3 undefined
}
}
```
`len` - Returns the number of key-value pairs in the hash.
```js
var myHash = new Hash();
myHash.set({
"key1": "value1",
"key2": "value2"
});
console.log(myHash:len()) // Output: 2
```
`all` - Returns a table containing all key-value pairs in the hash.
```js
var myHash = new Hash();
myHash.set({
"key1": "value1",
"key2": "value2"
});
var allKVPairs = myHash:all()
for (var key in allKVPairs) {
if (allKVPairs.hasOwnProperty(key)) {
console.log(key, allKVPairs[key]); // Output: key1 value1, key2 value2
}
}
```
`exists` - Checks if specified keys exist in the hash.
```js
var myHash = new Hash();
myHash.set({
"key1": "value1"
});
var existence = myHash.exists(["key1", "key2"])
for (var key in existence) {
if (existence.hasOwnProperty(key)) {
console.log(key, existence[key]); // Output: key1 true, key2 false
}
}
```
`del` - Deletes the specified keys from the hash. Returns the number of keys deleted.
```js
var myHash = new Hash();
myHash.set({
"key1": "value1",
"key2": "value2"
});
var numDeleted = myHash.del(["key1", "key3"])
console.log(numDeleted) // Output: 1
```
### Sets
The `set` data type is a custom data type in SugarDB designed for managing unique elements.
It supports operations like adding, removing, checking for membership,
and performing set operations such as subtraction.
This section explains how to use the `set` data type in your JavaScript modules.
#### Creating a Set
```js
var mySet1 = new Set(); // Create new empty set
var mySet2 = new Set(["apple", "banana", "cherry"]) // Create new set with elements
```
#### Set methods
`add` - Adds one or more elements to the set. Returns the number of elements added.
```js
var mySet = new Set();
var addedCount = mySet.add(["apple", "banana"])
console.log(addedCount) // Output: 2
```
`pop` - Removes and returns a specified number of random elements from the set.
```js
var mySet = new Set(["apple", "banana", "cherry"])
var popped = mySet.pop(2)
console.log(popped) // Outputs an array of 2 random elements from the set
```
`contains` - Checks if a specific element exists in the set.
```js
var mySet = new Set(["apple", "banana"])
console.log(mySet.contains("apple")) // Output: true
console.log(mySet.contains("cherry")) // Output: false
```
`cardinality` - Returns the number of elements in the set.
```js
var mySet = new Set(["apple", "banana"])
console.log(mySet.cardinality()) // Output: 2
```
`remove` - Removes one or more specified elements from the set. Returns the number of elements removed.
```js
var mySet = new Set(["apple", "banana", "cherry"])
var removedCount = mySet.remove(["banana", "cherry"])
console.log(removedCount) // Output: 2
```
`move` - Moves an element from one set to another. Returns true if the element was successfully moved.
```js
var set1 = new Set(["apple", "banana"])
var set2 = new Set(["cherry"])
var success = set1.move(set2, "banana")
console.log(success) // Output: true
```
`subtract` - Returns a new set that is the result of subtracting other sets from the current set.
```js
var set1 = new Set(["apple", "banana", "cherry"])
var set2 = new Set(["banana"])
var resultSet = set1.subtract([set2])
var allElems = resultSet.all()
for (var i = 0; i < allElems.length; i++) {
console.log(allElems[i]); // Output: "apple", "cherry"
}
```
`all` - Returns a table containing all elements in the set.
```js
var mySet = new Set(["apple", "banana", "cherry"])
var allElems = mySet.all()
for (var i = 0; i < allElems.length; i++) {
console.log(allElems[i]); // Output: "apple", "banana", "cherry"
}
```
`random` - Returns a table of randomly selected elements from the set. The number of elements to return is specified as an argument.
```js
var mySet = new Set(["apple", "banana", "cherry", "date"])
var randomElems = mySet.random(2)
console.log(randomElems) // Outputs an array of 2 random elements from the set
```
### Sorted Sets
A zset is a sorted set that stores zmember elements, ordered by their score.
The zset type provides methods to manipulate and query the set. A zset is made up of
zmember elements, each of which has a value and a score.
#### zmember
A zmember represents an element in a zset (sorted set). Each zmember consists of:
- value: A unique identifier for the member (e.g., a string).
- score: A numeric value used to sort the member in the sorted set.
You can create a zmember as follows:
```js
var m = new ZMember({value: "example", score: 42})
```
The zmember type provides methods to retrieve or modify these properties.
To set/get the value of a zmember, use the `value` method:
```js
// Get the value
var value = m.value()
// Set the value
m.value("new_value")
```
To set/get the score, use the `score` method:
```js
// Get the score
var score = m.score()
// Set the score
m.score(99.5)
```
#### Creating a Sorted Set
```js
// Create a new zset with no zmembers
var zset1 = new ZSet()
// Create a new zset with two zmembers
var zset2 = new ZSet([
new ZMember({value: "a", score: 10}),
new ZMember({value: "b", score: 20}),
])
```
#### Sorted Set Methods
`add` - Adds one or more zmember elements to the zset.
Optionally, you can specify update policies using the optional modifiers.
Optional Modifiers:
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
- "changed": If true, returns the count of changed elements.
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
Basic usage:
```js
// Create members
var m1 = new ZMember({value: "item1", score: 10})
var m2 = new ZMember({value: "item2", score: 20})
// Create zset and add members
var zset = new ZSet()
zset.add([m1, m2])
// Check cardinality
console.log(zset.cardinality()) // Outputs: 2
```
Usage with optional modifiers:
```js
// Create zset
var zset = new ZSet([
new ZMember({value: "a", score: 10}),
new ZMember({value: "b", score: 20}),
])
// Attempt to add members with different policies
var new_members = {
new ZMember({value: "a", score: 5}), // Existing member
new ZMember({value: "c", score: 15}), // New member
}
// Use policies to update and add
var options = {
exists = "xx", // Only update existing members
comparison = "max", // Keep the maximum score for existing members
changed = true, // Return the count of changed elements
}
var changed_count = zset.add(new_members, options)
// Display results
console.log("Changed count:", changed_count) // Outputs: 1 (only "a" is updated)
// Adding with different policies
var incr_options = {
exists = "nx", // Only add new members
incr = true, // Increment the score of the added members
}
zset.add([new ZMember({value: "d", score: 10})], incr_options)
```
`update` - Updates one or more zmember elements in the zset.
If the member doesnt exist, the behavior depends on the provided update options.
Optional Modifiers:
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
- "changed": If true, returns the count of changed elements.
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
```js
// Create members
var m1 = new ZMember({value: "item1", score: 10})
var m2 = new ZMember({value: "item2", score: 20})
// Create zset and add members
var zset = new ZSet([m1, m2])
// Update a member
var m_update = new ZMember({value: "item1", score: 15})
var changed_count = zset.update([m_update], {exists = true, comparison = "max", changed = true})
console.log("Changed count:", changed_count) // Outputs the number of elements updated
```
`remove` - Removes a member from the zset by its value.
```js
var removed = zset.remove("a") // Returns true if removed
```
`cardinality` - Returns the number of zmembers in the zset.
```js
var count = zset.cardinality()
```
`contains` - Checks if a zmember with the specified value exists in the zset.
```js
var exists = zset.contains("b") // Returns true if exists
```
`random` - Returns a random zmember from the zset.
```js
var members = zset.random(2) // Returns up to 2 random members
```
`all` - Returns all zmembers in the zset.
```js
var members = zset.all()
for (var i = 0; i < members.length; i++) {
console.log(members[i].value(), members[i].score())
}
```
`subtract` - Returns a new zset that is the result of subtracting other zsets from the current one.
```js
var other_zset = new ZSet([
new ZMember({value: "b", score: 20}),
])
var result_zset = zset.subtract([other_zset])
```

View File

@@ -44,9 +44,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do
print(k, v)
end
if (#command ~= 1) then if (#command ~= 1) then
error("wrong number of args, expected 0") error("wrong number of args, expected 0")
end end
@@ -138,8 +135,7 @@ Sugar DB supports the following standard data types in Lua scripts:
- nil - nil
- arrays (tables with integer keys) - arrays (tables with integer keys)
These data types can be used to stored using the setValues function and These data types can be stored using the setValues function and retrieved using the getValues function.
retrieved using the getValues function.
## Custom Data Types ## Custom Data Types
@@ -439,13 +435,7 @@ local options = {
changed = true, -- Return the count of changed elements changed = true, -- Return the count of changed elements
} }
local changed_count = zset:add(new_members, options) local changed_count = zset:add(new_members, options)
-- Display results
print("Changed count:", changed_count) -- Outputs: 1 (only "a" is updated) print("Changed count:", changed_count) -- Outputs: 1 (only "a" is updated)
print("Updated zset:")
for _, member in ipairs(zset:all()) do
print(member:value(), member:score())
end
-- Adding with different policies -- Adding with different policies
local incr_options = { local incr_options = {
@@ -453,12 +443,6 @@ local incr_options = {
incr = true, -- Increment the score of the added members incr = true, -- Increment the score of the added members
} }
zset:add({zmember.new({value = "d", score = 10})}, incr_options) zset:add({zmember.new({value = "d", score = 10})}, incr_options)
-- Display updated zset
print("After adding with increment:")
for _, member in ipairs(zset:all()) do
print(member:value(), member:score())
end
``` ```
`update` - Updates one or more zmember elements in the zset. `update` - Updates one or more zmember elements in the zset.

View File

@@ -1,9 +1,13 @@
import React from "react"; import React from "react";
import CodeBlock from "@theme/CodeBlock"; import CodeBlock from "@theme/CodeBlock";
const LoadModuleDocs = ({ module }: { module: "lua" | "go" }) => { const LoadModuleDocs = ({ module }: { module: "go" | "lua" | "js" }) => {
const module_path = const module_path =
module === "lua" ? "path/to/module/module.lua" : "path/to/module/module.so"; module === "go"
? "path/to/module/module.so"
: module === "lua"
? "path/to/module/module.lua"
: "path/to/module/module.js";
return ( return (
<div> <div>

3
go.mod
View File

@@ -30,8 +30,11 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.26 // indirect github.com/miekg/dns v1.1.26 // indirect
github.com/robertkrimen/otto v0.5.1 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
golang.org/x/crypto v0.14.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.16.0 // indirect golang.org/x/net v0.16.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
) )

6
go.sum
View File

@@ -112,6 +112,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
@@ -165,6 +167,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -174,6 +178,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -213,11 +213,6 @@ type HandlerFuncParams struct {
// scriptType is either "FILE" or "RAW". // scriptType is either "FILE" or "RAW".
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW" // content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW"
AddScript func(engine string, scriptType string, content string, args []string) error AddScript func(engine string, scriptType string, content string, args []string) error
// AddScriptCommand adds a commands to SugarDB that is defined by a scripting language.
// engine defines the interpreter to be used. Possible values: "LUA"
// scriptType is either "FILE" or "RAW".
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW".
AddScriptCommand func(content string, args []string) error
} }
// HandlerFunc is a functions described by a command where the bulk of the command handling is done. // HandlerFunc is a functions described by a command where the bulk of the command handling is done.

View File

@@ -0,0 +1,113 @@
// The keyword to trigger the command
var command = "JS.EXAMPLE"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["generic", "write", "fast"]
// The description of the command.
var description = "(JS.EXAMPLE) Example JS command that sets various data types to keys"
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
if (command.length > 1) {
throw "wrong number of args, expected 0"
}
return {
readKeys: [],
writeKeys: []
}
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Set various data types to keys
var keyValues = {
"numberKey": 42,
"stringKey": "Hello, SugarDB!",
"floatKey": 3.142,
"nilKey": null,
}
// Store the values in the database
setValues(keyValues)
// Verify the values have been set correctly
var keysToGet = ["numberKey", "stringKey", "floatKey", "nilKey"]
var retrievedValues = getValues(keysToGet)
// Create an array to track mismatches
var mismatches = [];
for (var key in keyValues) {
if (Object.prototype.hasOwnProperty.call(keyValues, key)) {
var expectedValue = keyValues[key];
var retrievedValue = retrievedValues[key];
if (retrievedValue !== expectedValue) {
var msg = "Key " + key + ": expected " + expectedValue + ", got " + retrievedValue
mismatches.push(msg);
console.log(msg)
}
}
}
// If mismatches exist, return an error
if (mismatches.length > 0) {
throw "values mismatch"
}
// If all values match, return OK
return "+OK\r\n"
}

View File

@@ -0,0 +1,136 @@
// The keyword to trigger the command
var command = "JS.HASH"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["hash", "write", "fast"]
// The description of the command.
var description = "(JS.HASH key) This is an example of working with SugarDB hashes/maps in js scripts."
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
if (command.length !== 2) {
throw "wrong number of args, expected 1."
}
return {
"readKeys": [],
"writeKeys": [command[1]]
}
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Initialize a new hash
var h = new Hash();
// Set values in the hash
h.set({
"field1": "value1",
"field2": "value2",
"field3": "value3",
"field4": "value4"
});
// Set hash in the store
var setVals = {}
setVals[command[1]] = h
setValues(setVals);
// Check that the fields were correctly set in the database
var hashValue = getValues([command[1]])[command[1]];
console.assert(hashValue.get(["field1"]).field1 === "value1", "field1 not set correctly");
console.assert(hashValue.get(["field2"]).field2 === "value2", "field2 not set correctly");
console.assert(hashValue.get(["field3"]).field3 === "value3", "field3 not set correctly");
console.assert(hashValue.get(["field4"]).field4 === "value4", "field4 not set correctly");
// Test get method
var retrieved = h.get(["field1", "field2"]);
console.assert(retrieved.field1 === "value1", "get method failed for field1");
console.assert(retrieved.field2 === "value2", "get method failed for field2");
// Test exists method
var exists = h.exists(["field1", "fieldX"]);
console.assert(exists.field1 === true, "exists method failed for field1");
console.assert(exists.fieldX === false, "exists method failed for fieldX");
// Test setnx method
var setnxCount = h.setnx({
"field1": "new_value1", // Should not overwrite
"field5": "value5" // Should set
});
console.assert(setnxCount === 1, "setnx did not set the correct number of fields");
console.assert(h.get(["field1"]).field1 === "value1", "setnx overwrote field1");
console.assert(h.get(["field5"]).field5 === "value5", "setnx failed to set field5");
// Test del method
var delCount = h.del(["field2", "field3"]);
console.assert(delCount === 2, "del did not delete the correct number of fields");
console.assert(h.exists(["field2"]).field2 === false, "del failed to delete field2");
console.assert(h.exists(["field3"]).field3 === false, "del failed to delete field3");
// Test len method
console.assert(h.len() === 3, "len method returned incorrect value");
// Retrieve and verify all remaining fields
var remainingFields = h.all();
console.assert(remainingFields.field1 === "value1", "field1 missing after deletion");
console.assert(remainingFields.field4 === "value4", "field4 missing after deletion");
console.assert(remainingFields.field5 === "value5", "field5 missing after deletion");
// Return RESP response
return "+OK\r\n";
}

View File

@@ -0,0 +1,128 @@
// The keyword to trigger the command
var command = "JS.LIST"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["list", "write", "fast"]
// The description of the command.
var description = "(JS.LIST key) This is an example of working with SugarDB lists in js scripts."
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
if (command.length !== 2) {
throw "wrong number of args, expected 4."
}
return {
"readKeys": [],
"writeKeys": [command[1]]
}
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Helper function to compare lists
function compareLists(expected, actual) {
if (expected.length !== actual.length) {
return {
isValid: false,
errorMessage: "Length mismatch: expected " + expected.length + ", got " + actual.length
};
}
for (var i = 0; i < expected.length; i++) {
if (expected[i] !== actual[i]) {
return {
isValid: false,
errorMessage: "Mismatch at index " + (i + 1) + ": expected " + expected[i] + ", got " + actual[i]
};
}
}
return { isValid: true };
}
var key = command[1]; // Adjusted for JavaScript's 0-based indexing
// First list to set
var initialList = ["apple", "banana", "cherry"];
var setVals = {}
setVals[key] = initialList
setValues(setVals);
// Retrieve and verify the first list
var retrievedValues = getValues([key]);
var retrievedList = retrievedValues[key];
var result = compareLists(initialList, retrievedList);
if (!result.isValid) {
throw new Error(result.errorMessage);
}
// Update the list with new values
var updatedList = ["orange", "grape", "watermelon"];
setVals = {}
setVals[key] = updatedList
setValues(setVals);
// Retrieve and verify the updated list
retrievedValues = getValues([key]);
retrievedList = retrievedValues[key];
result = compareLists(updatedList, retrievedList);
if (!result.isValid) {
throw result.errorMessage;
}
// If all assertions pass
return "+OK\r\n";
}

View File

@@ -0,0 +1,177 @@
// The keyword to trigger the command
var command = "JS.SET"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["set", "write", "fast"]
// The description of the command.
var description = "(JS.SET key member [member ...]]) " +
"This is an example of working with SugarDB sets in js scripts."
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
// Check the length of the command array
if (command.length < 3) {
throw new Error("wrong number of args, expected 2 or more");
}
// Return the result object
return {
readKeys: [],
writeKeys: [command[1], command[2], command[3]]
};
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Ensure there are enough arguments
if (command.length < 3) {
throw "wrong number of arguments, expected at least 3";
}
// Extract the keys
var key1 = command[1];
var key2 = command[2];
var key3 = command[3];
// Create two sets for testing `move` and `subtract`
var set1 = new Set(["elem1", "elem2", "elem3"]);
var set2 = new Set(["elem4", "elem5"]);
// Add elements to set1
set1.add(["elem6", "elem7"]);
// Check if an element exists in set1
var containsElem1 = set1.contains("elem1");
console.assert(containsElem1, "set1 does not contain expected element elem1")
var containsElemUnknown = set1.contains("unknown");
console.assert(!containsElemUnknown, "set1 contains unknown element")
// Get the size of set1
var set1Cardinality = set1.cardinality();
console.assert(set1Cardinality, "set1 cardinality expected 3, got " + set1Cardinality)
// Remove elements from set1
set1.remove(["elem1", "elem2"]);
var removedCount = 2; // Manually track removed count
// Pop elements from set1
set1.add(["elem1", "elem2"]);
var poppedElements = set1.pop(2);
console.assert(
poppedElements.length === 2,
"popped elements length must be 2, got " + poppedElements.length
)
// Get random elements from set1
var randomElements = set1.random(2);
console.assert(
randomElements.length === 2,
"random elements length must be 2, got " + randomElements.length
)
// Retrieve all elements from set1
var allElements = set1.all();
console.assert(
allElements.length === set1.cardinality(),
"all elements length must be " + set1.cardinality() + ", got " + allElements.length
)
// Move an element from set1 to set2
set1.add(["elem3"])
var moveSuccess = false;
if (set1.contains("elem3")) {
moveSuccess = set1.move(set2, "elem3");
}
console.assert(moveSuccess, "element not moved from set1 to set2")
// Verify that the element was moved
var set2ContainsMoved = set2.contains("elem3");
console.assert(set2ContainsMoved, "set2 does not contain expected element after move")
var set1NoLongerContainsMoved = !set1.contains("elem3");
console.assert(set1NoLongerContainsMoved, "set1 still contains unexpected element after move")
// Subtract set2 from set1
var resultSet = set1.subtract([set2]);
// Store the modified sets
var setVals = {}
setVals[key1] = set1
setVals[key2] = set2
setVals[key3] = resultSet
setValues(setVals);
// Retrieve the sets back to verify storage
var storedValues = getValues([key1, key2, key3]);
var storedSet1 = storedValues[key1];
var storedSet2 = storedValues[key2];
var storedResultSet = storedValues[key3];
// Perform additional checks to ensure consistency
if (!storedSet1 || storedSet1.size !== set1.size) {
throw "Stored set1 does not match the modified set1";
}
if (!storedSet2 || storedSet2.size !== set2.size) {
throw "Stored set2 does not match the modified set2";
}
if (!storedResultSet || storedResultSet.size !== resultSet.size) {
throw "Stored result set does not match the computed result set";
}
// If all operations succeed, return "OK"
return "+OK\r\n";
}

View File

@@ -0,0 +1,170 @@
// The keyword to trigger the command
var command = "JS.ZSET"
// The string array of categories this command belongs to.
// This array can contain both built-in categories and new custom categories.
var categories = ["sortedset", "write", "fast"]
// The description of the command.
var description = "(JS.ZSET key member score [member score ...]) " +
"This is an example of working with SugarDB sorted sets in js scripts."
// Whether the command should be synced across the RAFT cluster.
var sync = true
/**
* keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
* The returned data from this function is used in the Access Control Layer to determine if the current connection is
* authorized to execute this command. The function must return a table that specifies which keys in this command
* are read keys and which ones are write keys.
* Example return: {readKeys: ["key1", "key2"], writeKeys: ["key3", "key4", "key5"]}
*
* 1. "command" is a string array representing the command that triggered this key extraction function.
*
* 2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
* These args are passed to the key extraction function everytime it's invoked.
*/
function keyExtractionFunc(command, args) {
if (command.length < 4) {
throw "wrong number of args, expected 3 or more";
}
return {
readKeys: [],
writeKeys: [command[1], command[2], command[3]]
};
}
/**
* handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
* SugarDB. The function must return a valid RESP response or throw an error.
* The handler function accepts the following args:
*
* 1. "context" is a table that contains some information about the environment this command has been executed in.
* Example: {protocol: 2, database: 0}
* This object contains the following properties:
* i) protocol - the protocol version of the client that executed the command (either 2 or 3).
* ii) database - the active database index of the client that executed the command.
*
* 2. "command" is the string array representing the command that triggered this handler function.
*
* 3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
* This function accepts a string array of keys to check and returns a table with each key having a corresponding
* boolean value indicating whether it exists.
* Examples:
* i) Example invocation: keyExists(["key1", "key2", "key3"])
* ii) Example return: {key1: true, key2: false, key3: true}
*
* 4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
* The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
* containing the corresponding value from the store.
* The possible data types for the values are: number, string, nil, hash, set, zset
* Examples:
* i) Example invocation: getValues(["key1", "key2", "key3"])
* ii) Example return: {key1: 3.142, key2: nil, key3: "Pi"}
*
* 5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
* This function accepts a table with keys and the corresponding values to set for each key in the active database
* in the store.
* The accepted data types for the values are: number, string, nil, hash, set, zset.
* The setValues function does not return anything.
* Examples:
* i) Example invocation: setValues({key1: 3.142, key2: nil, key3: "Pi"})
*
* 6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
* handler everytime it's invoked.
*/
function handlerFunc(ctx, command, keysExist, getValues, setValues, args) {
// Ensure there are enough arguments
if (command.length < 4) {
throw new Error("wrong number of arguments, expected at least 3");
}
var key1 = command[1];
var key2 = "key2";
var key3 = "key3";
// Create `ZMember` instances
var member1 = new ZMember({ value: "member1", score: 10 });
var member2 = new ZMember({ value: "member2", score: 20 });
var member3 = new ZMember({ value: "member3", score: 30 });
// Create a `ZSet` and add initial members
var zset1 = new ZSet(member1, member2);
// Test `add` method with a new member
zset1.add([member3]);
// Test `update` method by modifying an existing member
zset1.update([new ZMember({ value: "member1", score: 15 })]);
// Test `remove` method
zset1.remove("member2");
// Test `cardinality` method
var zset1Cardinality = zset1.cardinality();
console.assert(zset1Cardinality === 2, "zset1 expected cardinality is 2, got " + zset1Cardinality)
// Test `contains` method
var containsMember3 = zset1.contains("member3");
console.assert(containsMember3, "zset1 does not contain expected member member3")
var containsNonExistent = zset1.contains("nonexistent");
console.assert(!containsNonExistent, "zset1 contains unexpected element 'nonexistent'")
// Test `random` method
var randomMembers = zset1.random(2);
console.assert(
randomMembers.length === 2,
"zset1 random members result should be length 2, got " + randomMembers.length
)
// Test `all` method
var allMembers = zset1.all();
console.assert(
allMembers.length === zset1.cardinality(),
"zset1 'all' did not return expected cardinality of " + zset1.cardinality + ", got " + allMembers.length
)
// Create another `ZSet` to test `subtract` manually
var zset2 = new ZSet(new ZMember({ value: "member3", score: 30 }));
// Subtract the zset2 from zset1
var resultZSet = zset1.subtract([zset2])
// Store the `ZSet` objects in SugarDB
var setVals = {}
setVals[key1] = zset1
setVals[key2] = zset2
setVals[key3] = resultZSet
setValues(setVals);
// Retrieve the stored `ZSet` objects to verify storage
var storedValues = getValues([key1, key2, key3]);
var storedZset1 = storedValues[key1];
var storedZset2 = storedValues[key2];
var storedZset3 = storedValues[key3];
// Perform consistency checks
if (!storedZset1 || storedZset1.cardinality() !== zset1.cardinality()) {
throw "Stored zset1 does not match the modified zset1";
}
if (!storedZset2 || storedZset2.cardinality() !== zset2.cardinality()) {
throw "Stored zset2 does not match the modified zset2";
}
if (!storedZset3 || storedZset3.cardinality() !== resultZSet.cardinality()) {
throw "Stored result zset does not match the computed result zset"
}
// Test `ZMember` methods
var memberValue = member1.value();
member1.value("updated_member1");
var updatedValue = member1.value();
console.assert(updatedValue !== memberValue, "updated member value still the same as old value")
var memberScore = member1.score();
member1.score(50);
var updatedScore = member1.score();
console.assert(updatedScore !== memberScore, "updated member score still the same as old score")
// Return an "OK" response
return "+OK\r\n";
}

View File

@@ -27,9 +27,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do
print(k, v)
end
if (#command ~= 1) then if (#command ~= 1) then
error("wrong number of args, expected 0") error("wrong number of args, expected 0")
end end

View File

@@ -28,9 +28,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do
print(k, v)
end
if (#command < 2) then if (#command < 2) then
error("wrong number of args, expected 1") error("wrong number of args, expected 1")
end end

View File

@@ -27,9 +27,6 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do
print(k, v)
end
if (#command < 2) then if (#command < 2) then
error("wrong number of args, expected 1") error("wrong number of args, expected 1")
end end

View File

@@ -28,11 +28,8 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do if (#command < 3) then
print(k, v) error("wrong number of args, expected 2 or more")
end
if (#command < 4) then
error("wrong number of args, expected 3")
end end
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} } return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
end end

View File

@@ -28,10 +28,7 @@ Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4
These args are passed to the key extraction function everytime it's invoked. These args are passed to the key extraction function everytime it's invoked.
]] ]]
function keyExtractionFunc (command, args) function keyExtractionFunc (command, args)
for k,v in pairs(args) do if (#command < 4) then
print(k, v)
end
if (#command ~= 4) then
error("wrong number of args, expected 2") error("wrong number of args, expected 2")
end end
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} } return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }

View File

@@ -391,6 +391,51 @@ func TestSugarDB_Plugins(t *testing.T) {
want: "OK", want: "OK",
wantErr: nil, wantErr: nil,
}, },
{
name: "9. Test JS module that handles primitive types",
path: path.Join("..", "internal", "volumes", "modules", "js", "example.js"),
expect: true,
args: []string{},
cmd: []string{"JS.EXAMPLE"},
want: "OK",
wantErr: nil,
},
{
name: "10. Test JS module that handles hashes",
path: path.Join("..", "internal", "volumes", "modules", "js", "hash.js"),
expect: true,
args: []string{},
cmd: []string{"JS.HASH", "JS_HASH_KEY1"},
want: "OK",
wantErr: nil,
},
{
name: "11. Test JS module that handles sets",
path: path.Join("..", "internal", "volumes", "modules", "js", "set.js"),
expect: true,
args: []string{},
cmd: []string{"JS.SET", "JS_SET_KEY1", "member1"},
want: "OK",
wantErr: nil,
},
{
name: "12. Test JS module that handles sorted sets",
path: path.Join("..", "internal", "volumes", "modules", "js", "zset.js"),
expect: true,
args: []string{},
cmd: []string{"JS.ZSET", "JS_ZSET_KEY1", "member1", "2.142"},
want: "OK",
wantErr: nil,
},
{
name: "13. Test JS module that handles lists",
path: path.Join("..", "internal", "volumes", "modules", "js", "list.js"),
expect: true,
args: []string{},
cmd: []string{"JS.LIST", "JS_LIST_KEY1"},
want: "OK",
wantErr: nil,
},
} }
for _, test := range tests { for _, test := range tests {

View File

@@ -69,7 +69,6 @@ func (server *SugarDB) getHandlerFuncParams(ctx context.Context, cmd []string, c
SwapDBs: server.SwapDBs, SwapDBs: server.SwapDBs,
GetServerInfo: server.GetServerInfo, GetServerInfo: server.GetServerInfo,
AddScript: server.AddScript, AddScript: server.AddScript,
AddScriptCommand: server.AddScriptCommand,
DeleteKey: func(ctx context.Context, key string) error { DeleteKey: func(ctx context.Context, key string) error {
server.storeLock.Lock() server.storeLock.Lock()
defer server.storeLock.Unlock() defer server.storeLock.Unlock()
@@ -160,8 +159,8 @@ func (server *SugarDB) handleCommand(ctx context.Context, message []byte, conn *
} }
if conn != nil && server.acl != nil && !embedded { if conn != nil && server.acl != nil && !embedded {
// Authorize connection if it's provided and if ACL module is present // Authorize connection if it's provided and if ACL module is present and the embedded parameter is false.
// and the embedded parameter is false. // Skip the authorization if the command is being executed from embedded mode.
if err = server.acl.AuthorizeConnection(conn, cmd, command, subCommand); err != nil { if err = server.acl.AuthorizeConnection(conn, cmd, command, subCommand); err != nil {
return nil, err return nil, err
} }

View File

@@ -39,10 +39,12 @@ func (server *SugarDB) AddScriptCommand(
var engine string var engine string
if strings.HasSuffix(path, ".lua") { if strings.HasSuffix(path, ".lua") {
engine = "lua" engine = "lua"
} else if strings.HasSuffix(path, ".js") {
engine = "js"
} }
// Check if the engine is supported // Check if the engine is supported
supportedEngines := []string{"lua"} supportedEngines := []string{"lua", "js"}
if !slices.Contains(supportedEngines, strings.ToLower(engine)) { if !slices.Contains(supportedEngines, strings.ToLower(engine)) {
return fmt.Errorf("engine %s not supported, only %v engines are supported", engine, supportedEngines) return fmt.Errorf("engine %s not supported, only %v engines are supported", engine, supportedEngines)
} }
@@ -59,6 +61,8 @@ func (server *SugarDB) AddScriptCommand(
switch strings.ToLower(engine) { switch strings.ToLower(engine) {
case "lua": case "lua":
vm, commandName, categories, description, synchronize, commandType, err = generateLuaCommandInfo(path) vm, commandName, categories, description, synchronize, commandType, err = generateLuaCommandInfo(path)
case "js":
vm, commandName, categories, description, synchronize, commandType, err = generateJSCommandInfo(path)
} }
if err != nil { if err != nil {
@@ -85,7 +89,7 @@ func (server *SugarDB) AddScriptCommand(
Description: description, Description: description,
Sync: synchronize, Sync: synchronize,
Type: commandType, Type: commandType,
KeyExtractionFunc: func(engine string, vm any, args []string) internal.KeyExtractionFunc { KeyExtractionFunc: func(engine string, args []string) internal.KeyExtractionFunc {
// Wrapper for the key function // Wrapper for the key function
return func(cmd []string) (internal.KeyExtractionFuncResult, error) { return func(cmd []string) (internal.KeyExtractionFuncResult, error) {
switch strings.ToLower(engine) { switch strings.ToLower(engine) {
@@ -96,21 +100,25 @@ func (server *SugarDB) AddScriptCommand(
WriteKeys: make([]string, 0), WriteKeys: make([]string, 0),
}, nil }, nil
case "lua": case "lua":
return server.buildLuaKeyExtractionFunc(vm, cmd, args) return server.luaKeyExtractionFunc(cmd, args)
case "js":
return server.jsKeyExtractionFunc(cmd, args)
} }
} }
}(engine, vm, args), }(engine, args),
HandlerFunc: func(engine string, vm any, args []string) internal.HandlerFunc { HandlerFunc: func(engine string, args []string) internal.HandlerFunc {
// Wrapper that generates handler function // Wrapper that generates handler function
return func(params internal.HandlerFuncParams) ([]byte, error) { return func(params internal.HandlerFuncParams) ([]byte, error) {
switch strings.ToLower(engine) { switch strings.ToLower(engine) {
default: default:
return nil, fmt.Errorf("command %s handler not implemented", commandName) return nil, fmt.Errorf("command %s handler not implemented", commandName)
case "lua": case "lua":
return server.buildLuaHandlerFunc(vm, commandName, args, params) return server.luaHandlerFunc(commandName, args, params)
case "js":
return server.jsHandlerFunc(commandName, args, params)
} }
} }
}(engine, vm, args), }(engine, args),
} }
// Add the commands to the list of commands. // Add the commands to the list of commands.
@@ -131,7 +139,7 @@ func (server *SugarDB) LoadModule(path string, args ...string) error {
server.commandsRWMut.Lock() server.commandsRWMut.Lock()
defer server.commandsRWMut.Unlock() defer server.commandsRWMut.Unlock()
for _, suffix := range []string{".lua"} { for _, suffix := range []string{".lua", ".js"} {
if strings.HasSuffix(path, suffix) { if strings.HasSuffix(path, suffix) {
return server.AddScriptCommand(path, args) return server.AddScriptCommand(path, args)
} }

View File

@@ -0,0 +1,908 @@
// Copyright 2024 Kelvin Clement Mwinuka
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sugardb
import (
"errors"
"fmt"
"github.com/echovault/sugardb/internal"
"github.com/echovault/sugardb/internal/modules/hash"
"github.com/echovault/sugardb/internal/modules/set"
"github.com/echovault/sugardb/internal/modules/sorted_set"
"github.com/robertkrimen/otto"
"math"
"os"
"reflect"
"slices"
"strings"
"sync"
"sync/atomic"
)
var (
objectRegistry sync.Map
idCounter uint64
)
func registerObject(object interface{}) string {
id := fmt.Sprintf("id-%d", atomic.AddUint64(&idCounter, 1))
objectRegistry.Store(id, object)
return id
}
func getObjectById(id string) (interface{}, bool) {
return objectRegistry.Load(id)
}
func clearObjectRegistry() {
atomic.StoreUint64(&idCounter, 0)
objectRegistry.Clear()
}
func generateJSCommandInfo(path string) (*otto.Otto, string, []string, string, bool, string, error) {
// Initialize the Otto vm
vm := otto.New()
// Load JS file
content, err := os.ReadFile(path)
if err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not load javascript script file %s: %v", path, err)
}
if _, err = vm.Run(content); err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not run javascript script file %s: %v", path, err)
}
// Register hash data type
_ = vm.Set("Hash", func(call otto.FunctionCall) otto.Value {
// Initialize hash
h := hash.Hash{}
// If an object is passed then initialize the default values of the hash
if len(call.ArgumentList) > 0 {
args := call.Argument(0).Object()
for _, key := range args.Keys() {
value, _ := args.Get(key)
v, _ := value.ToString()
h[key] = hash.HashValue{Value: v}
}
}
obj, _ := call.Otto.Object(`({})`)
buildHashObject(obj, h)
return obj.Value()
})
// Register set data type
_ = vm.Set("Set", func(call otto.FunctionCall) otto.Value {
// Initialize set
s := set.NewSet([]string{})
// If an array is passed add the values to the set
if len(call.ArgumentList) > 0 {
args := call.Argument(0).Object()
var elems []string
for _, key := range args.Keys() {
value, _ := args.Get(key)
v, _ := value.ToString()
elems = append(elems, v)
}
s.Add(elems)
}
obj, _ := call.Otto.Object(`({})`)
buildSetObject(obj, s)
return obj.Value()
})
// Register sorted set member data type
_ = vm.Set("ZMember", func(call otto.FunctionCall) otto.Value {
obj, _ := call.Otto.Object(`({})`)
m := &sorted_set.MemberParam{}
if len(call.ArgumentList) != 1 {
panicWithFunctionCall(call, "expected an object with score and value properties")
}
arg := call.Argument(0).Object()
// Validate the object
if err = validateMemberParamObject(arg); err != nil {
panicWithFunctionCall(call, err.Error())
}
// Get the value
value, _ := arg.Get("value")
m.Value = sorted_set.Value(value.String())
// Get the score
s, _ := arg.Get("score")
score, _ := s.ToFloat()
m.Score = sorted_set.Score(score)
// Build the Otto member param object
buildMemberParamObject(obj, m)
return obj.Value()
})
// Register sorted set data type
_ = vm.Set("ZSet", func(call otto.FunctionCall) otto.Value {
// If default args are passed when initializing sorted set, add them to the member params
var params []sorted_set.MemberParam
for _, arg := range call.ArgumentList {
if !arg.IsObject() {
panicWithFunctionCall(call, "zset constructor args must be sorted set members")
}
id, _ := arg.Object().Get("__id")
o, exists := getObjectById(id.String())
if !exists {
panicWithFunctionCall(call, "unknown object passed to zset constructor")
}
p, ok := o.(*sorted_set.MemberParam)
if !ok {
panicWithFunctionCall(call, "unknown object passed to createZSet function")
}
params = append(params, *p)
}
ss := sorted_set.NewSortedSet(params)
obj, _ := call.Otto.Object(`({})`)
buildSortedSetObject(obj, ss)
return obj.Value()
})
// Get the command name
v, err := vm.Get("command")
if err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command %s: %v", path, err)
}
command, err := v.ToString()
if err != nil || len(command) <= 0 {
return nil, "", nil, "", false, "", fmt.Errorf("javascript command not found %s: %v", path, err)
}
// Get the categories
v, err = vm.Get("categories")
if err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command categories %s: %v", path, err)
}
isArray, _ := vm.Run(`Array.isArray(categories)`)
if ok, _ := isArray.ToBoolean(); !ok {
return nil, "", nil, "", false, "", fmt.Errorf("javascript command categories is not an array %s: %v", path, err)
}
c, _ := v.Export()
categories := c.([]string)
// Get the description
v, err = vm.Get("description")
if err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command description %s: %v", path, err)
}
description, err := v.ToString()
if err != nil || len(description) <= 0 {
return nil, "", nil, "", false, "", fmt.Errorf("javascript command description not found %s: %v", path, err)
}
// Get the sync policy
v, err = vm.Get("sync")
if err != nil {
return nil, "", nil, "", false, "", fmt.Errorf("could not get javascript command sync policy %s: %v", path, err)
}
if !v.IsBoolean() {
return nil, "", nil, "", false, "", fmt.Errorf("javascript command sync policy is not a boolean %s: %v", path, err)
}
synchronize, _ := v.ToBoolean()
// Set command type
commandType := "JS_SCRIPT"
return vm, strings.ToLower(command), categories, description, synchronize, commandType, nil
}
// jsKeyExtractionFunc executes the extraction function defined in the script and returns the result or error.
func (server *SugarDB) jsKeyExtractionFunc(cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
// Lock the script before executing the key extraction function.
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
if !ok {
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", cmd[0])
}
machine := script.(struct {
vm any
lock *sync.Mutex
})
machine.lock.Lock()
defer machine.lock.Unlock()
vm := machine.vm.(*otto.Otto)
f, _ := vm.Get("keyExtractionFunc")
if !f.IsFunction() {
return internal.KeyExtractionFuncResult{}, errors.New("keyExtractionFunc is not a function")
}
v, err := f.Call(f, cmd, args)
if err != nil {
return internal.KeyExtractionFuncResult{}, err
}
if !v.IsObject() {
return internal.KeyExtractionFuncResult{}, errors.New("keyExtractionFunc return type is not an object")
}
data := v.Object()
rk, _ := data.Get("readKeys")
rkv, _ := rk.Export()
readKeys, ok := rkv.([]string)
if !ok {
if _, ok = rkv.([]interface{}); !ok {
return internal.KeyExtractionFuncResult{}, fmt.Errorf("readKeys for command %s is not an array", cmd[0])
}
readKeys = []string{}
}
wk, _ := data.Get("writeKeys")
wkv, _ := wk.Export()
writeKeys, ok := wkv.([]string)
if !ok {
if _, ok = wkv.([]interface{}); !ok {
return internal.KeyExtractionFuncResult{}, fmt.Errorf("writeKeys for command %s is not an array", cmd[0])
}
writeKeys = []string{}
}
return internal.KeyExtractionFuncResult{
Channels: make([]string, 0),
ReadKeys: readKeys,
WriteKeys: writeKeys,
}, nil
}
// jsHandlerFunc executes the extraction function defined in the script nad returns the RESP response or error.
func (server *SugarDB) jsHandlerFunc(command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
// Lock the script before executing the key extraction function.
script, ok := server.scriptVMs.Load(strings.ToLower(command))
if !ok {
return nil, fmt.Errorf("no lock found for script command %s", command)
}
machine := script.(struct {
vm any
lock *sync.Mutex
})
machine.lock.Lock()
defer machine.lock.Unlock()
vm := machine.vm.(*otto.Otto)
f, _ := vm.Get("handlerFunc")
if !f.IsFunction() {
return nil, errors.New("handlerFunc is not a function")
}
v, err := f.Call(
f,
// Build context
func() otto.Value {
obj, _ := vm.Object(`({})`)
_ = obj.Set("protocol", params.Context.Value("Protocol").(int))
_ = obj.Set("database", params.Context.Value("Database").(int))
return obj.Value()
}(),
// Command
params.Command,
// Build keysExist function
func(keys []string) otto.Value {
obj, _ := vm.Object(`({})`)
exists := server.keysExist(params.Context, keys)
for key, value := range exists {
_ = obj.Set(key, value)
}
return obj.Value()
},
// Build getValues function
func(keys []string) otto.Value {
obj, _ := vm.Object(`({})`)
values := server.getValues(params.Context, keys)
for key, value := range values {
switch value.(type) {
default:
_ = obj.Set(key, value)
case nil:
_ = obj.Set(key, otto.NullValue())
case []string:
l, _ := vm.Object(`([])`)
for i, elem := range value.([]string) {
_ = l.Set(fmt.Sprintf("%d", i), elem)
}
_ = obj.Set(key, l.Value())
case hash.Hash:
h, _ := vm.Object(`({})`)
buildHashObject(h, value.(hash.Hash))
_ = obj.Set(key, h.Value())
case *set.Set:
s, _ := vm.Object(`({})`)
buildSetObject(s, value.(*set.Set))
_ = obj.Set(key, s.Value())
case *sorted_set.SortedSet:
ss, _ := vm.Object(`({})`)
buildSortedSetObject(ss, value.(*sorted_set.SortedSet))
_ = obj.Set(key, ss.Value())
}
}
return obj.Value()
},
// Build setValues function
func(entries map[string]interface{}) {
values := make(map[string]interface{})
for key, entry := range entries {
switch entry.(type) {
default:
panicInHandler(fmt.Sprintf("unknown type %s on key %s", reflect.TypeOf(entry).String(), key))
case nil:
values[key] = nil
case string:
values[key] = internal.AdaptType(entry.(string))
case int64:
values[key] = int(entry.(int64))
case float64:
values[key] = entry.(float64)
case []string:
values[key] = entry.([]string)
case map[string]interface{}:
value, ok := entry.(map[string]interface{})
if !ok || value["__id"] == nil {
panicInHandler(fmt.Sprintf("unknown object on key %s", key))
}
obj, exists := getObjectById(value["__id"].(string))
if !exists {
panicInHandler(
fmt.Sprintf(
"could not find object of id %s in the object registry on key %s",
value["__id"].(string),
key,
),
)
}
switch obj.(type) {
default:
panicInHandler(fmt.Sprintf("unknown type on key %s for command %s\n", key, command))
case hash.Hash:
values[key] = obj.(hash.Hash)
case *set.Set:
values[key] = obj.(*set.Set)
case *sorted_set.SortedSet:
values[key] = obj.(*sorted_set.SortedSet)
}
}
}
if err := server.setValues(params.Context, values); err != nil {
panicInHandler(err.Error())
}
},
// Args
args,
)
if err != nil {
return nil, err
}
res, err := v.ToString()
clearObjectRegistry()
return []byte(res), err
}
func buildHashObject(obj *otto.Object, h hash.Hash) {
_ = obj.Set("__type", "hash")
_ = obj.Set("__id", registerObject(h))
_ = obj.Set("set", func(call otto.FunctionCall) otto.Value {
args := call.Argument(0).Object()
for _, key := range args.Keys() {
value, _ := args.Get(key)
v, _ := value.ToString()
h[key] = hash.HashValue{Value: v}
}
// Return changed count using the set data type
count, _ := otto.ToValue(set.NewSet(args.Keys()).Cardinality())
return count
})
_ = obj.Set("setnx", func(call otto.FunctionCall) otto.Value {
count := 0
args := call.Argument(0).Object()
for _, key := range args.Keys() {
if _, exists := h[key]; exists {
continue
}
count += 1
value, _ := args.Get(key)
v, _ := value.ToString()
h[key] = hash.HashValue{Value: v}
}
c, _ := otto.ToValue(count)
return c
})
_ = obj.Set("get", func(call otto.FunctionCall) otto.Value {
result, _ := call.Otto.Object(`({})`)
for _, arg := range call.ArgumentList {
key, _ := arg.ToString()
value, _ := otto.ToValue(h[key].Value)
_ = result.Set(key, value)
}
return result.Value()
})
_ = obj.Set("len", func(call otto.FunctionCall) otto.Value {
length, _ := otto.ToValue(len(h))
return length
})
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
result, _ := call.Otto.Object(`({})`)
for key, value := range h {
v, _ := otto.ToValue(value.Value)
_ = result.Set(key, v)
}
return result.Value()
})
_ = obj.Set("exists", func(call otto.FunctionCall) otto.Value {
result, _ := call.Otto.Object(`({})`)
for _, arg := range call.ArgumentList {
key, _ := arg.ToString()
_, ok := h[key]
exists, _ := call.Otto.ToValue(ok)
_ = result.Set(key, exists)
}
return result.Value()
})
_ = obj.Set("del", func(call otto.FunctionCall) otto.Value {
count := 0
for _, arg := range call.ArgumentList {
key, _ := arg.ToString()
if _, exists := h[key]; exists {
count += 1
delete(h, key)
}
}
result, _ := otto.ToValue(count)
return result
})
}
func buildSetObject(obj *otto.Object, s *set.Set) {
_ = obj.Set("__type", "set")
_ = obj.Set("__id", registerObject(s))
_ = obj.Set("add", func(call otto.FunctionCall) otto.Value {
args := call.Argument(0).Object()
if args == nil {
panicWithFunctionCall(call, "set add method argument not an object")
}
var elems []string
for _, key := range args.Keys() {
value, _ := args.Get(key)
v, _ := value.ToString()
elems = append(elems, v)
}
count := s.Add(elems)
result, _ := otto.ToValue(count)
return result
})
_ = obj.Set("pop", func(call otto.FunctionCall) otto.Value {
count, _ := call.Argument(0).ToInteger()
popped := s.Pop(int(count))
result, _ := call.Otto.Object(`([])`)
_ = result.Set("length", len(popped))
for i, p := range popped {
_ = result.Set(fmt.Sprintf("%d", i), p)
}
return result.Value()
})
_ = obj.Set("contains", func(call otto.FunctionCall) otto.Value {
value, _ := call.Argument(0).ToString()
result, _ := otto.ToValue(s.Contains(value))
return result
})
_ = obj.Set("cardinality", func(call otto.FunctionCall) otto.Value {
result, _ := otto.ToValue(s.Cardinality())
return result
})
_ = obj.Set("remove", func(call otto.FunctionCall) otto.Value {
args := call.Argument(0).Object()
if args == nil {
panicWithFunctionCall(call, "set remove method argument not an object")
}
var elems []string
for _, key := range args.Keys() {
value, _ := args.Get(key)
v, _ := value.ToString()
elems = append(elems, v)
}
result, _ := otto.ToValue(s.Remove(elems))
return result
})
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
all := s.GetAll()
result, _ := call.Otto.Object(`([])`)
_ = result.Set("length", len(all))
for i, e := range all {
_ = result.Set(fmt.Sprintf("%d", i), e)
}
return result.Value()
})
_ = obj.Set("random", func(call otto.FunctionCall) otto.Value {
count, _ := call.Argument(0).ToInteger()
random := s.GetRandom(int(count))
result, _ := call.Otto.Object(`([])`)
_ = result.Set("length", len(random))
for i, r := range random {
_ = result.Set(fmt.Sprintf("%d", i), r)
}
return result.Value()
})
_ = obj.Set("move", func(call otto.FunctionCall) otto.Value {
arg := call.Argument(0).Object()
elem := call.Argument(1).String()
id, _ := arg.Get("__id")
o, exists := getObjectById(id.String())
if !exists {
panicWithFunctionCall(call, "move target set does not exist")
}
switch o.(type) {
default:
panicWithFunctionCall(call, "move target is not a set")
case *set.Set:
moved := s.Move(o.(*set.Set), elem) == 1
result, _ := otto.ToValue(moved)
return result
}
return otto.NullValue()
})
_ = obj.Set("subtract", func(call otto.FunctionCall) otto.Value {
extractSets := func(call otto.FunctionCall) ([]*set.Set, error) {
var sets []*set.Set
if len(call.ArgumentList) > 1 {
return sets, fmt.Errorf("set subtract method expects 1 arg, got %d", len(call.ArgumentList))
}
arg1 := call.Argument(0).Object()
if arg1.Class() != "Array" {
return sets, errors.New("set subtract method expects the first argument to be an array")
}
for _, key := range arg1.Keys() {
// Check if the array element is a valid MemberParam type.
argMember, _ := arg1.Get(key)
if !argMember.IsObject() {
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
}
// Get the member param from the object registry
argMemberObj := argMember.Object()
id, _ := argMemberObj.Get("__id")
o, exists := getObjectById(id.String())
if !exists {
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
}
m, ok := o.(*set.Set)
if !ok {
panicWithFunctionCall(call, "set subtract method first arg must be an array of valid sets")
}
sets = append(sets, m)
}
return sets, nil
}
sets, err := extractSets(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
diff := s.Subtract(sets)
result, _ := call.Otto.Object(`({})`)
buildSetObject(result, diff)
return result.Value()
})
}
func buildMemberParamObject(obj *otto.Object, m *sorted_set.MemberParam) {
_ = obj.Set("__type", "zmember")
_ = obj.Set("__id", registerObject(m))
_ = obj.Set("value", func(call otto.FunctionCall) otto.Value {
switch len(call.ArgumentList) {
case 0:
// If no value is passed, then return the current value
v, _ := otto.ToValue(m.Value)
return v
case 1:
// If a value is passed, then set the value
v := call.Argument(0).String()
if len(v) <= 0 {
panicWithFunctionCall(call, "zset member value must be a non-empty string")
}
m.Value = sorted_set.Value(v)
default:
panicWithFunctionCall(
call,
fmt.Sprintf(
"expected either 0 or 1 args for value method of zmember, got %d",
len(call.ArgumentList),
),
)
}
return otto.NullValue()
})
_ = obj.Set("score", func(call otto.FunctionCall) otto.Value {
switch len(call.ArgumentList) {
case 0:
s, _ := otto.ToValue(m.Score)
return s
case 1:
s, _ := call.Argument(0).ToFloat()
if math.IsNaN(s) {
panicWithFunctionCall(call, "zset member score must be a valid number")
}
m.Score = sorted_set.Score(s)
default:
panicWithFunctionCall(
call,
fmt.Sprintf(
"expected either 0 or 1 args for score method of zmember, got %d",
len(call.ArgumentList),
),
)
}
return otto.NullValue()
})
}
func validateMemberParamObject(obj *otto.Object) error {
value, _ := obj.Get("value")
if slices.Contains([]otto.Value{otto.UndefinedValue(), otto.NullValue()}, value) ||
len(value.String()) == 0 {
return errors.New("zset member value must be a non-empty string")
}
s, _ := obj.Get("score")
if slices.Contains([]otto.Value{otto.UndefinedValue(), otto.NullValue()}, s) {
return errors.New("zset member must have a score")
}
score, _ := s.ToFloat()
if math.IsNaN(score) {
return errors.New("zset member score must be a valid number")
}
return nil
}
func buildSortedSetObject(obj *otto.Object, ss *sorted_set.SortedSet) {
// Function to extract member param arguments for "add" and "update" methods.
extractMembers := func(call otto.FunctionCall) ([]sorted_set.MemberParam, error) {
var members []sorted_set.MemberParam
if !call.Argument(0).IsObject() {
return members, errors.New("zset add or update method expects the first argument to be an array")
}
arg1 := call.Argument(0).Object()
if arg1.Class() != "Array" {
return members, errors.New("zset add or update method expects the first argument to be an array")
}
for _, key := range arg1.Keys() {
// Check if the array element is a valid MemberParam type.
argMember, _ := arg1.Get(key)
if !argMember.IsObject() {
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
}
// Get the member param from the object registry
argMemberObj := argMember.Object()
id, _ := argMemberObj.Get("__id")
o, exists := getObjectById(id.String())
if !exists {
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
}
m, ok := o.(*sorted_set.MemberParam)
if !ok {
panicWithFunctionCall(call, "zset add or update method first arg must be an array of valid zmembers")
}
members = append(members, *m)
}
return members, nil
}
// Function to build and verify the update policy for "add" and "update" methods
type updateModifiers struct {
updatePolicy interface{}
comparison interface{}
changed interface{}
incr interface{}
}
extractUpdateModifiers := func(call otto.FunctionCall) (updateModifiers, error) {
modifiers := updateModifiers{updatePolicy: nil, comparison: nil, changed: nil, incr: nil}
if len(call.ArgumentList) < 2 {
return modifiers, nil
}
if !call.Argument(1).IsObject() {
return modifiers, errors.New("zset add or update method second arg must be an object")
}
arg2 := call.Argument(1).Object()
acceptedKeys := []string{"exists", "comparison", "changed", "incr"}
for _, key := range arg2.Keys() {
if !slices.Contains(acceptedKeys, key) {
return modifiers, fmt.Errorf(
"zset add or update method second arg unknown key '%s', expected %+v", key, acceptedKeys)
}
v, _ := arg2.Get(key)
switch key {
case "exists":
if !v.IsBoolean() {
return modifiers, errors.New("zset add or update method second arg 'exists' key should be a boolean")
}
exists, _ := v.ToBoolean()
if exists {
modifiers.updatePolicy = "xx"
} else {
modifiers.updatePolicy = "nx"
}
case "comparison":
modifiers.comparison = v.String()
case "changed":
if !v.IsBoolean() {
return modifiers, errors.New("zset add or update method second arg 'changed' key should be a boolean")
}
changed, _ := v.ToBoolean()
modifiers.changed = changed
case "incr":
if !v.IsBoolean() {
return modifiers, errors.New("zset add or update method second arg 'incr' key should be a boolean")
}
incr, _ := v.ToBoolean()
modifiers.incr = incr
}
}
return modifiers, nil
}
_ = obj.Set("__type", "zset")
_ = obj.Set("__id", registerObject(ss))
_ = obj.Set("add", func(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) < 1 || len(call.ArgumentList) > 2 {
panicWithFunctionCall(call, fmt.Sprintf("zset add method expects 1 or 2 args, got %d", len(call.ArgumentList)))
}
// Extract the member params from the first arg
members, err := extractMembers(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
// Extract the modifiers in the second arg, if they are passed.
modifiers, err := extractUpdateModifiers(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
count, err := ss.AddOrUpdate(
members,
modifiers.updatePolicy,
modifiers.comparison,
modifiers.changed,
modifiers.incr,
)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
v, _ := call.Otto.ToValue(count)
return v
})
_ = obj.Set("update", func(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) < 1 || len(call.ArgumentList) > 2 {
panicWithFunctionCall(call, fmt.Sprintf("zset update method expects 1 or 2 args, got %d", len(call.ArgumentList)))
}
// Extract the member params from the first arg
members, err := extractMembers(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
// Extract the modifiers in the second arg, if they are passed.
modifiers, err := extractUpdateModifiers(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
count, err := ss.AddOrUpdate(
members,
modifiers.updatePolicy,
modifiers.comparison,
modifiers.changed,
modifiers.incr,
)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
v, _ := call.Otto.ToValue(count)
return v
})
_ = obj.Set("remove", func(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
panicWithFunctionCall(call, fmt.Sprintf("zset remove method expects 1 ard, got %d", len(call.ArgumentList)))
}
value := sorted_set.Value(call.Argument(0).String())
v, _ := call.Otto.ToValue(ss.Remove(value))
return v
})
_ = obj.Set("cardinality", func(call otto.FunctionCall) otto.Value {
value, _ := otto.ToValue(ss.Cardinality())
return value
})
_ = obj.Set("contains", func(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
panicWithFunctionCall(call, fmt.Sprintf("zset contains method expects 1 arg, got %d", len(call.ArgumentList)))
}
v, _ := otto.ToValue(ss.Contains(sorted_set.Value(call.Argument(0).String())))
return v
})
_ = obj.Set("random", func(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
panicWithFunctionCall(call, fmt.Sprintf("zset random method expects 1 arg, got %d", len(call.ArgumentList)))
}
count, _ := call.Argument(0).ToInteger()
var paramValues []otto.Value
for _, p := range ss.GetRandom(int(count)) {
m, _ := call.Otto.Object(`({})`)
buildMemberParamObject(m, &p)
paramValues = append(paramValues, m.Value())
}
p, _ := call.Otto.ToValue(paramValues)
return p
})
_ = obj.Set("all", func(call otto.FunctionCall) otto.Value {
var paramValues []otto.Value
for _, p := range ss.GetAll() {
m, _ := call.Otto.Object(`({})`)
buildMemberParamObject(m, &p)
paramValues = append(paramValues, m.Value())
}
p, _ := call.Otto.ToValue(paramValues)
return p
})
_ = obj.Set("subtract", func(call otto.FunctionCall) otto.Value {
extractZSets := func(call otto.FunctionCall) ([]*sorted_set.SortedSet, error) {
var zsets []*sorted_set.SortedSet
if len(call.ArgumentList) > 1 {
return zsets, fmt.Errorf("zset subtract method expects 1 arg, got %d", len(call.ArgumentList))
}
arg1 := call.Argument(0).Object()
if arg1.Class() != "Array" {
return zsets, errors.New("zset subtract method expects the first argument to be an array")
}
for _, key := range arg1.Keys() {
// Check if the array element is a valid MemberParam type.
argMember, _ := arg1.Get(key)
if !argMember.IsObject() {
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
}
// Get the member param from the object registry
argMemberObj := argMember.Object()
id, _ := argMemberObj.Get("__id")
o, exists := getObjectById(id.String())
if !exists {
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
}
m, ok := o.(*sorted_set.SortedSet)
if !ok {
panicWithFunctionCall(call, "zset subtract method first arg must be an array of valid zsets")
}
zsets = append(zsets, m)
}
return zsets, nil
}
zsets, err := extractZSets(call)
if err != nil {
panicWithFunctionCall(call, err.Error())
}
diff := ss.Subtract(zsets)
result, _ := call.Otto.Object(`({})`)
buildSortedSetObject(result, diff)
return result.Value()
})
}
func panicWithFunctionCall(call otto.FunctionCall, message string) {
err, _ := call.Otto.ToValue(message)
panic(err)
}
func panicInHandler(message string) {
value, _ := otto.ToValue(message)
panic(value)
}

View File

@@ -638,8 +638,21 @@ func generateLuaCommandInfo(path string) (*lua.LState, string, []string, string,
return L, strings.ToLower(cn.String()), categories, d.String(), synchronize, commandType, nil return L, strings.ToLower(cn.String()), categories, d.String(), synchronize, commandType, nil
} }
func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []string) (internal.KeyExtractionFuncResult, error) { // luaKeyExtractionFunc executes the extraction function defined in the script and returns the result or error.
L := vm.(*lua.LState) func (server *SugarDB) luaKeyExtractionFunc(cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
// Lock the script before executing the key extraction function
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
if !ok {
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", cmd[0])
}
machine := script.(struct {
vm any
lock *sync.Mutex
})
machine.lock.Lock()
defer machine.lock.Unlock()
L := machine.vm.(*lua.LState)
// Create command table to pass to the Lua function // Create command table to pass to the Lua function
command := L.NewTable() command := L.NewTable()
for i, s := range cmd { for i, s := range cmd {
@@ -650,17 +663,7 @@ func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []st
for i, s := range args { for i, s := range args {
funcArgs.RawSetInt(i+1, lua.LString(s)) funcArgs.RawSetInt(i+1, lua.LString(s))
} }
// Lock the script before executing the key extraction function
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
if !ok {
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", command)
}
machine := script.(struct {
vm any
lock *sync.Mutex
})
machine.lock.Lock()
defer machine.lock.Unlock()
// Call the Lua key extraction function // Call the Lua key extraction function
var err error var err error
_ = L.CallByParam(lua.P{ _ = L.CallByParam(lua.P{
@@ -706,8 +709,21 @@ func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []st
} }
} }
func (server *SugarDB) buildLuaHandlerFunc(vm any, command string, args []string, params internal.HandlerFuncParams) ([]byte, error) { // luaHandlerFunc executes the extraction function defined in the script nad returns the RESP response or error.
L := vm.(*lua.LState) func (server *SugarDB) luaHandlerFunc(command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
// Lock this script's execution key before executing the handler.
script, ok := server.scriptVMs.Load(command)
if !ok {
return nil, fmt.Errorf("no lock found for script command %s", command)
}
machine := script.(struct {
vm any
lock *sync.Mutex
})
machine.lock.Lock()
defer machine.lock.Unlock()
L := machine.vm.(*lua.LState)
// Lua table context // Lua table context
ctx := L.NewTable() ctx := L.NewTable()
ctx.RawSetString("protocol", lua.LNumber(params.Context.Value("Protocol").(int))) ctx.RawSetString("protocol", lua.LNumber(params.Context.Value("Protocol").(int)))
@@ -786,17 +802,7 @@ func (server *SugarDB) buildLuaHandlerFunc(vm any, command string, args []string
for i, s := range args { for i, s := range args {
funcArgs.RawSetInt(i+1, lua.LString(s)) funcArgs.RawSetInt(i+1, lua.LString(s))
} }
// Lock this script's execution key before executing the handler
script, ok := server.scriptVMs.Load(command)
if !ok {
return nil, fmt.Errorf("no lock found for script command %s", command)
}
lock := script.(struct {
vm any
lock *sync.Mutex
})
lock.lock.Lock()
defer lock.lock.Unlock()
// Call the lua handler function // Call the lua handler function
var err error var err error
_ = L.CallByParam(lua.P{ _ = L.CallByParam(lua.P{

View File

@@ -105,7 +105,7 @@ type SugarDB struct {
commandsRWMut sync.RWMutex // Mutex used for modifying/reading the list of commands in the instance. commandsRWMut sync.RWMutex // Mutex used for modifying/reading the list of commands in the instance.
commands []internal.Command // Holds the list of all commands supported by SugarDB. commands []internal.Command // Holds the list of all commands supported by SugarDB.
// Each commands that's added using a script (e.g. lua), will have a lock associated with the command. // Each commands that's added using a script (lua,js), will have a lock associated with the command.
// Only one goroutine will be able to trigger a script-associated command at a time. This is because the VM state // Only one goroutine will be able to trigger a script-associated command at a time. This is because the VM state
// for each of the commands is not thread safe. // for each of the commands is not thread safe.
// This map's shape is map[string]struct{vm: any, lock: sync.Mutex} with the string key being the command name. // This map's shape is map[string]struct{vm: any, lock: sync.Mutex} with the string key being the command name.
@@ -639,7 +639,7 @@ func (server *SugarDB) ShutDown() {
log.Println("shutting down script vms...") log.Println("shutting down script vms...")
server.commandsRWMut.Lock() server.commandsRWMut.Lock()
for _, command := range server.commands { for _, command := range server.commands {
if slices.Contains([]string{"LUA_SCRIPT"}, command.Type) { if slices.Contains([]string{"LUA_SCRIPT", "JS_SCRIPT"}, command.Type) {
v, ok := server.scriptVMs.Load(command.Command) v, ok := server.scriptVMs.Load(command.Command)
if !ok { if !ok {
continue continue