Learn how to use class wrappers for cleaner, object-oriented plugin APIs in JavaScript.
Classes in Plugify provide an object-oriented way to work with plugin resources, offering lifecycle management and cleaner APIs. Instead of manually managing handles and calling functions with explicit pointers, you can use intuitive class-based interfaces.
Working with handle-based APIs in the traditional C-style can be verbose and error-prone:
Without Classes (C-style)
import * as s2sdk from ':s2sdk';
// Create a KeyValues handle manually
const kv_handle = s2sdk.Kv1Create("MyConfig");
// Set properties using the handle
s2sdk.Kv1SetName(kv_handle, "ServerSettings");
// Find a subkey
const subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players");
// Easy to forget cleanup!
s2sdk.Kv1Destroy(subkey_handle);
s2sdk.Kv1Destroy(kv_handle);
With classes, the same code becomes much cleaner:
With Classes (OOP-style)
import * as s2sdk from ':s2sdk';
// Create using a class constructor
const kv = new s2sdk.KeyValues("MyConfig");
// Use intuitive methods
kv.SetName("ServerSettings");
// Find a subkey - returns a KeyValues instance
const subkey = kv.FindKey("Players");
// Must manually clean up!
subkey.close();
kv.close();
Benefits:
Cleaner syntax - Methods instead of functions with explicit handles
Type safety - Better IDE autocomplete with TypeScript definitions
Less error-prone - Harder to mix up handles
Automatic validation - Methods check handle validity before calling
JavaScript-idiomatic API - Feels natural to JavaScript developers
Important Limitation:
No Deterministic Cleanup: Unlike Python or Lua, JavaScript does not have deterministic resource cleanup. You must call close() explicitly or rely on non-deterministic garbage collection finalization.
Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:
Using get()
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
// Get the raw handle
const raw_handle = kv.get();
console.log(`Handle value: ${raw_handle}`);
// Pass to a function expecting a raw handle
some_c_function(kv.get());
Warning: Be careful when using get(). The returned handle is still owned by the class instance and will be destroyed when the instance is cleaned up.
Releases ownership of the handle and returns it. After calling release(), the class instance becomes invalid and will not call the destructor:
Using release()
import * as s2sdk from ':s2sdk';
function create_and_release() {
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// Transfer ownership out
const handle = kv.release();
// kv is now invalid, won't clean up
console.log(kv.valid()); // false
return handle;
}
// We now own the handle and must clean it up manually
const raw_handle = create_and_release();
// ... use raw_handle ...
s2sdk.Kv1Destroy(raw_handle); // Manual cleanup required!
Explicitly closes the handle and calls the destructor if the object owns it:
Using close()
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// Explicitly close the handle now
kv.close();
// Handle is now invalid
console.log(kv.valid()); // false
// Methods will throw error
try {
kv.SetName("Test");
} catch (e) {
console.error(`Error: ${e.message}`); // "KeyValues handle is closed"
}
The close() method is:
Idempotent - Safe to call multiple times
Unregisters from finalizer - Prevents double-cleanup
Ownership-aware - Only calls destructor if object owns the handle
Required for immediate cleanup - JavaScript has no deterministic destructors
Critical: Always call close() explicitly in JavaScript. Do not rely on garbage collection for timely resource cleanup.
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// Reset is equivalent to close
kv.reset();
console.log(kv.valid()); // false
JavaScript has no deterministic resource cleanup mechanism. You must explicitly call close():
Manual Cleanup
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
try {
// ... use kv ...
} finally {
// CRITICAL: Always clean up in finally block
kv.close();
}
}
processConfig();
Best practice: Always use try-finally blocks to ensure cleanup:
Try-Finally Pattern
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
try {
kv.SetName("ServerConfig");
kv.SetString("hostname", "My Server");
// ... use kv ...
} finally {
kv.close(); // Guaranteed cleanup even if exceptions occur
}
}
Plugify uses JavaScript's FinalizationRegistry internally to cleanup resources when objects are garbage collected. However, this is non-deterministic and should not be relied upon:
Finalization Fallback
import * as s2sdk from ':s2sdk';
function processConfig() {
const kv = new s2sdk.KeyValues("Config");
kv.SetName("ServerConfig");
// ... use kv ...
// NO close() call - relies on finalization (BAD!)
}
processConfig();
// kv will eventually be cleaned up by FinalizationRegistry
// but timing is unpredictable - DO NOT RELY ON THIS!
Warning: FinalizationRegistry cleanup timing is unpredictable and may occur long after the object is no longer reachable. Always call close() explicitly for timely resource cleanup.
If a class doesn't have a destructor defined in the manifest, it acts as a simple wrapper without automatic cleanup:
Wrapper Without Destructor
import * as s2sdk from ':s2sdk';
// Class with no destructor - just a convenience wrapper
const wrapper = new s2sdk.SomeWrapper();
// Still has utility methods
if (wrapper.valid()) {
const handle = wrapper.get();
}
// No automatic cleanup - handle persists
// Useful for stateless wrappers or global resources
When a method takes ownership of a resource, you should transfer it and not use it afterward:
Ownership Transfer
import * as s2sdk from ':s2sdk';
// Create parent and child
const parent = new s2sdk.KeyValues("Parent");
const child = new s2sdk.KeyValues("Child");
try {
// AddSubKey takes ownership of child
parent.AddSubKey(child);
// child is now owned by parent
// child.valid() may still be true, but don't use it!
// parent will handle cleanup
} finally {
parent.close();
}
Best practice: After transferring ownership, avoid using the object:
Proper Ownership Handling
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.KeyValues("Parent");
const child = new s2sdk.KeyValues("Child");
try {
// Transfer ownership
parent.AddSubKey(child);
// Release our reference to prevent accidental use
child.release(); // Now child.valid() === false
// Only use through parent
const found = parent.FindKey("Child");
if (found) {
found.close();
}
} finally {
parent.close();
}
When a method returns a new resource with ownership:
Return Ownership
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.KeyValues("Parent");
try {
// FindKey returns a NEW KeyValues that we own
const child = parent.FindKey("Settings");
if (child && child.valid()) {
try {
child.SetName("UpdatedSettings");
// We're responsible for child's lifecycle
} finally {
child.close(); // Must clean up returned object
}
}
} finally {
parent.close();
}
When owner: false, the method returns a reference without transferring ownership:
Non-Owning Reference
import * as s2sdk from ':s2sdk';
const parent = new s2sdk.KeyValues("Parent");
try {
// GetFirstSubKey returns a reference, parent still owns it
const child_ref = parent.GetFirstSubKey();
if (child_ref && child_ref.valid()) {
// Use the reference, but DON'T close it
const name = child_ref.GetName();
// Don't call child_ref.close() or child_ref.release()
// child_ref will be cleaned up by parent
}
} finally {
parent.close();
}
Important: With non-owning references, the returned object is still valid (has a handle), but you don't own it. Closing the parent will invalidate these references.
When using classes in plugins, you must be careful about object cleanup during plugin unload. If objects are still alive when your plugin is unloaded, their finalizers may be called after the plugin's code is no longer in memory, leading to undefined behavior or crashes.
If you store class instances in global variables or module-level objects, you must explicitly clean them up in your plugin's pluginEnd() function:
plugin.mjs
import { Plugin } from 'plugify';
import * as s2sdk from ':s2sdk';
// Global object - dangerous if not cleaned up!
let g_config = null;
export class MyPlugin extends Plugin {
pluginStart() {
// Create global configuration
g_config = new s2sdk.KeyValues("GlobalConfig");
g_config.SetName("ServerSettings");
console.log("Plugin started with global config");
}
pluginEnd() {
// CRITICAL: Clean up global objects before plugin unload
if (g_config) {
g_config.close();
g_config = null;
}
console.log("Plugin ended, global config cleaned up");
}
}
Critical: Failing to clean up global objects in pluginEnd() may cause finalizers to run after your plugin is unloaded, resulting in crashes or undefined behavior. Always explicitly clean up global resources!
export class MyPlugin extends Plugin {
pluginStart() {
this.config = new s2sdk.KeyValues("PluginConfig");
this.config.SetName("Settings");
}
pluginEnd() {
// Clean up instance variables
if (this.config) {
this.config.close();
this.config = null;
}
}
}
❌ Unsafe: Global without cleanup
Unsafe Pattern
import * as s2sdk from ':s2sdk';
// DANGEROUS: Global object
const g_config = new s2sdk.KeyValues("GlobalConfig");
export class MyPlugin extends Plugin {
pluginStart() {
g_config.SetName("ServerSettings");
}
pluginEnd() {
// MISSING: No cleanup!
// g_config finalizer will run after plugin unload - CRASH!
}
}
❌ Unsafe: Module-level cache without cleanup
Unsafe Pattern
import * as s2sdk from ':s2sdk';
// DANGEROUS: Module-level cache
const kv_cache = new Map();
export class MyPlugin extends Plugin {
cacheConfig(name) {
kv_cache.set(name, new s2sdk.KeyValues(name));
}
pluginEnd() {
// MISSING: No cache cleanup!
// Cached objects will finalize after plugin unload - CRASH!
}
}
Always use try-finally - Wrap resource usage in try-finally blocks for guaranteed cleanup
Call close() explicitly - JavaScript has no deterministic destructors, manual cleanup is required
Clean up global objects in pluginEnd() - Critical: Explicitly clean up all global or module-level class instances before plugin unload to avoid crashes
Don't rely on FinalizationRegistry - Finalization timing is unpredictable, always use close()
Check valid() for safety - Especially after operations that might fail
Respect ownership - Don't use objects after transferring ownership
Use release() sparingly - Only when you need manual control
Avoid mixing styles - Prefer class API over raw C-style functions
Handle errors - Be prepared to catch errors from methods called on closed handles
Check returned objects - Methods might return null on failure
Use TypeScript - Type definitions help catch errors at development time
Attempting to use an object after calling close() or release() will throw an error:
Using Closed Handle
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
kv.close();
// This throws an error
try {
kv.SetName("Test");
} catch (e) {
console.error(e.message); // "KeyValues handle is closed"
}
// Check valid() first to avoid exceptions
if (kv.valid()) {
kv.SetName("Test");
} else {
console.log("Handle is closed!");
}
All bound methods automatically validate the handle before calling the underlying C function:
Automatic Validation
import * as s2sdk from ':s2sdk';
const kv = new s2sdk.KeyValues("Config");
const handle = kv.release();
// Any method call will fail
try {
const name = kv.GetName();
} catch (e) {
console.error(e.message); // "KeyValues handle is closed"
}
Be careful with non-owning references when the owner is destroyed:
Dangling Reference Problem
import * as s2sdk from ':s2sdk';
function getChildRef() {
const parent = new s2sdk.KeyValues("Parent");
const child = parent.GetFirstSubKey(); // Non-owning reference
parent.close();
return child; // BAD: parent was destroyed!
}
// This is dangerous!
const child_ref = getChildRef();
// child_ref now points to destroyed memory
// Better approach:
function getChildOwned() {
const parent = new s2sdk.KeyValues("Parent");
try {
const child = parent.FindKey("Child"); // Returns owned instance
// Need to keep parent alive or ensure child is used properly
return child; // Caller must close both parent and child
} catch (e) {
parent.close();
throw e;
}
}
Check global objects - Ensure all global class instances are cleaned up in pluginEnd()
Check module-level collections - Clear any Map, Set, or Array containing class instances
Check instance variables - Clean up class instance variables in pluginEnd()
Add explicit cleanup - Use close() on all long-lived objects before plugin unload
Fix Unload Crash
import * as s2sdk from ':s2sdk';
// Global objects that caused crashes
let g_config = null;
const g_cache = new Map();
export class MyPlugin extends Plugin {
pluginStart() {
g_config = new s2sdk.KeyValues("Config");
g_cache.set("main", new s2sdk.KeyValues("Main"));
}
pluginEnd() {
// Clean up global objects
if (g_config) {
g_config.close();
g_config = null;
}
// Clean up cached objects
for (const [key, kv] of g_cache) {
kv.close();
}
g_cache.clear();
console.log("All resources cleaned up safely");
}
}