Using Classes

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.

Why Use Classes?

Working with handle-based APIs in the traditional C-style can be verbose and error-prone:

Without Classes (C-style)
import * as example_plugin from ':example_plugin';

// Create a KeyValues handle manually
const kv_handle = example_plugin.Kv1Create("MyConfig");

// Set properties using the handle
example_plugin.Kv1SetName(kv_handle, "ServerSettings");

// Find a subkey
const subkey_handle = example_plugin.Kv1FindKey(kv_handle, "Players");

// Easy to forget cleanup!
example_plugin.Kv1Destroy(kv_handle);

With classes, the same code becomes much cleaner:

With Classes (OOP-style)
import * as example_plugin from ':example_plugin';

// Create using a class constructor
const kv = new example_plugin.KeyValues("MyConfig");

// Use intuitive methods
kv.SetName("ServerSettings");

// Find a subkey - returns a borrowed KeyValues, or null if there is none
const subkey = kv.FindKey("Players");

// Must manually clean up!
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:

How Classes Work

When a plugin defines classes in its manifest, the JavaScript language module builds wrapper classes for them in the plugin's synthetic module (':example_plugin'). The wrappers:

  1. Wrap the underlying handle - Store the raw pointer internally
  2. Bind methods - Convert function calls to method calls with automatic handle passing
  3. Manage lifecycle - Register with FinalizationRegistry for eventual cleanup
  4. Provide utility methods - get(), release(), valid(), close(), reset() for handle management
  5. Validate handles - Automatically check handle validity before method calls
  6. Provide type hints - Generate .d.ts TypeScript definition files for IDE support

Defining Classes in Your Manifest

To create classes for your plugin, add a classes section to your manifest:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    { "name": "Kv1Create", "funcName": "Kv1Create", "paramTypes": [{ "name": "setName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1Destroy", "funcName": "Kv1Destroy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1GetName", "funcName": "Kv1GetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "string" } },
    { "name": "Kv1SetName", "funcName": "Kv1SetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "name", "type": "string" }], "retType": { "type": "void" } },
    { "name": "Kv1FindKey", "funcName": "Kv1FindKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetFirstSubKey", "funcName": "Kv1GetFirstSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1AddSubKey", "funcName": "Kv1AddSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "subKey", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1MakeCopy", "funcName": "Kv1MakeCopy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetString", "funcName": "Kv1GetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "defaultValue", "type": "string" }], "retType": { "type": "string" } },
    { "name": "Kv1SetString", "funcName": "Kv1SetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "value", "type": "string" }], "retType": { "type": "void" } }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        { "name": "GetName", "method": "Kv1GetName", "bindSelf": true },
        { "name": "SetName", "method": "Kv1SetName", "bindSelf": true },
        { "name": "FindKey", "method": "Kv1FindKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "GetFirstSubKey", "method": "Kv1GetFirstSubKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "AddSubKey", "method": "Kv1AddSubKey", "bindSelf": true, "paramAliases": [{ "name": "KeyValues", "owner": true }] },
        { "name": "MakeCopy", "method": "Kv1MakeCopy", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": true } },
        { "name": "GetString", "method": "Kv1GetString", "bindSelf": true },
        { "name": "SetString", "method": "Kv1SetString", "bindSelf": true }
      ]
    }
  ]
}

Key fields explained:

  • name: The JavaScript class name (PascalCase recommended)
  • handleType: The type of the underlying handle (usually ptr64 or ptr32)
  • invalidValue: What value represents an invalid handle (usually "0" or "-1")
  • constructors: Array of method names that create instances
  • destructor: Method name that cleans up resources (optional)
  • bindings: Array of methods available on the class
    • bindSelf: If true, automatically passes the handle as the first parameter

Generated JavaScript Code

When you define classes in your manifest, the language module builds wrapper classes with several built-in methods. Conceptually, they look like this:

Generated Class (Conceptual)
class KeyValues {
    constructor(...args) {
        // Initialize to invalid state
        this._handle = 0;  // invalidValue
        this._owned = Ownership.BORROWED;

        // Direct handle construction
        // Pattern: new KeyValues(handle_value, Ownership.OWNED)
        if (args.length >= 2 && Ownership.is(args[1])) {
            this._handle = args[0];
            this._owned = args[1];

            // Register for finalization if owned
            if (this._owned === Ownership.OWNED) {
                finalizerRegistry.register(this, {
                    handle: this._handle,
                    owned: this._owned,
                    destructor: _plugin.Kv1Destroy,
                    invalidValue: 0
                }, this);
            }
            return;
        }

        // Constructor call mode
        try {
            this._handle = _plugin.Kv1Create(...args);
            this._owned = Ownership.OWNED;

            // Register for finalization
            finalizerRegistry.register(this, {
                handle: this._handle,
                owned: this._owned,
                destructor: _plugin.Kv1Destroy,
                invalidValue: 0
            }, this);
        } catch (e) {
            throw e;
        }
    }

    close() {
        if (this._handle !== 0 && this._owned === Ownership.OWNED) {
            _plugin.Kv1Destroy(this._handle);
            // Unregister from finalizer
            finalizerRegistry.unregister(this);
        }
        this._handle = 0;
        this._owned = Ownership.BORROWED;
    }

    // Utility methods
    get() {
        return this._handle;
    }

    release() {
        if (this._owned === Ownership.OWNED) {
            finalizerRegistry.unregister(this);
        }
        const tmp = this._handle;
        this._handle = 0;
        this._owned = Ownership.BORROWED;
        return tmp;
    }

    reset() {
        this.close();
    }

    valid() {
        return this._handle !== 0;
    }

    // Bound methods (with automatic handle validation)
    GetName() {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        return _plugin.Kv1GetName(this._handle);
    }

    SetName(name) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        _plugin.Kv1SetName(this._handle, name);
    }

    FindKey(keyName) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        const result = _plugin.Kv1FindKey(this._handle, keyName);
        // Automatically wraps return value based on retAlias
        if (result !== 0) {
            return new KeyValues(result, Ownership.OWNED);
        }
        return null;
    }

    AddSubKey(subKey) {
        if (this._handle === 0) {
            throw new Error("KeyValues handle is closed or not initialized");
        }
        // Automatically releases ownership from subKey parameter
        const handle = (typeof subKey.release === 'function') ? subKey.release() : subKey;
        _plugin.Kv1AddSubKey(this._handle, handle);
    }
}

plugify-gen -lang v8 -classes generates a matching .d.ts TypeScript definition file for IDE support:

plugin.d.ts
import { Ownership } from 'plugify';

export class KeyValues {
    constructor(setName: string);
    constructor(handle: bigint, ownership: Ownership);

    close(): void;
    get(): bigint;
    release(): bigint;
    reset(): void;
    valid(): boolean;

    GetName(): string;
    SetName(name: string): void;
    FindKey(keyName: string): KeyValues | null;
    AddSubKey(subKey: KeyValues): void;
}

Built-in Utility Methods

Every generated class includes several utility methods for handle management:

valid() - Check Handle Validity

Returns true if the handle is valid (not equal to invalidValue):

Using valid()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");

if (kv.valid()) {
    kv.SetName("ServerConfig");
    console.log("Handle is valid");
} else {
    console.log("Handle is invalid");
}

// After release, handle becomes invalid
const handle = kv.release();
console.log(kv.valid());  // false

get() - Access Raw Handle

Returns the underlying handle value. Use this when you need to pass the raw handle to C-style functions:

Using get()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.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
example_plugin.Kv1GetName(kv.get());

release() - Transfer Ownership

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 example_plugin from ':example_plugin';

function create_and_release() {
    const kv = new example_plugin.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 ...
example_plugin.Kv1Destroy(raw_handle);  // Manual cleanup required!

Use release() when you need to:

  • Transfer ownership to another system
  • Store the handle in a long-lived data structure
  • Interface with C-style code that takes ownership

close() - Manual Cleanup

Explicitly closes the handle and calls the destructor if the object owns it:

Using close()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.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

reset() - Alias for Close

Convenience method that calls close():

Using reset()
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.KeyValues("Config");
kv.SetName("ServerConfig");

// Reset is equivalent to close
kv.reset();

console.log(kv.valid());  // false

Resource Management

JavaScript classes generated by Plugify require manual resource management:

Manual Cleanup (Required)

JavaScript has no deterministic resource cleanup mechanism. You must explicitly call close():

Manual Cleanup
import * as example_plugin from ':example_plugin';

function processConfig() {
    const kv = new example_plugin.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 example_plugin from ':example_plugin';

function processConfig() {
    const kv = new example_plugin.KeyValues("Config");

    try {
        kv.SetName("ServerConfig");
        kv.SetString("hostname", "My Server");
        // ... use kv ...
    } finally {
        kv.close();  // Guaranteed cleanup even if exceptions occur
    }
}

FinalizationRegistry (Non-Deterministic Fallback)

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 example_plugin from ':example_plugin';

function processConfig() {
    const kv = new example_plugin.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!

No Destructor Case

If a class doesn't have a destructor defined in the manifest, it acts as a simple wrapper without automatic cleanup: close() only resets the handle, and the utility methods still work.

Working with Ownership

Some methods transfer ownership of resources. The manifest specifies this with the owner field in paramAliases and retAlias.

Taking Ownership (Method Parameters)

When a method takes ownership of a resource, you should transfer it and not use it afterward:

Ownership Transfer
import * as example_plugin from ':example_plugin';

// Create parent and child
const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");

try {
    // AddSubKey takes ownership of child
    parent.AddSubKey(child);

    // AddSubKey released the handle from child: child.valid() is now false
    // parent will handle cleanup
} finally {
    parent.close();
}

In the manifest, this is defined as:

Manifest Ownership
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "KeyValues",
      "owner": true  // This parameter takes ownership
    }
  ]
}

Best practice: After transferring ownership, avoid using the object:

Proper Ownership Handling
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.KeyValues("Parent");
const child = new example_plugin.KeyValues("Child");

try {
    // Transfer ownership
    parent.AddSubKey(child);

    console.log(`valid: ${child.valid()}`);  // valid: false, the wrapper gave up the handle

    // Only use through parent
    const found = parent.FindKey("Child");
    if (found) {
        found.close();
    }
} finally {
    parent.close();
}

Returning Ownership (Return Values)

When a method returns a new resource with ownership:

Return Ownership
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.KeyValues("Parent");

try {
    // MakeCopy returns a NEW KeyValues that we own
    const copy = parent.MakeCopy();

    if (copy && copy.valid()) {
        try {
            copy.SetName("ParentCopy");
            // We're responsible for copy's lifecycle
        } finally {
            copy.close();  // Must clean up returned object
        }
    }
} finally {
    parent.close();
}

In the manifest:

Manifest Return Ownership
{
  "name": "MakeCopy",
  "method": "Kv1MakeCopy",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Caller owns the returned object
  }
}

Non-Owning References

When owner: false, the method returns a reference without transferring ownership:

Non-Owning Reference
import * as example_plugin from ':example_plugin';

const parent = new example_plugin.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();
}

Plugin Lifecycle and Global Objects

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.

Managing Global Objects

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 example_plugin from ':example_plugin';

// Global object - dangerous if not cleaned up!
let g_config = null;

export class MyPlugin extends Plugin {
    pluginStart() {
        // Create global configuration
        g_config = new example_plugin.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");
    }
}

Safe Patterns

✅ Safe: Try-finally with cleanup

Safe Pattern
export class MyPlugin extends Plugin {
    onCommand(args) {
        const kv = new example_plugin.KeyValues("TempConfig");

        try {
            kv.SetName("CommandConfig");
            // ... use kv ...
        } finally {
            kv.close();  // Guaranteed cleanup - safe!
        }
    }
}

✅ Safe: Instance variable with cleanup

Safe Pattern
export class MyPlugin extends Plugin {
    pluginStart() {
        this.config = new example_plugin.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 example_plugin from ':example_plugin';

// DANGEROUS: Global object
const g_config = new example_plugin.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 example_plugin from ':example_plugin';

// DANGEROUS: Module-level cache
const kv_cache = new Map();

export class MyPlugin extends Plugin {
    cacheConfig(name) {
        kv_cache.set(name, new example_plugin.KeyValues(name));
    }

    pluginEnd() {
        // MISSING: No cache cleanup!
        // Cached objects will finalize after plugin unload - CRASH!
    }
}

Cleanup Checklist

Before your plugin unloads (pluginEnd()), ensure:

  1. ✅ All global class instances are explicitly closed or set to null
  2. ✅ All module-level collections (Map, Set, Array) containing class instances are cleared
  3. ✅ All class instance variables are explicitly cleaned up
  4. ✅ No references to class instances remain in any long-lived data structures

Complete Example: Configuration System

Here's a complete example showing how to use classes for a configuration system:

config_manager.mjs
import * as example_plugin from ':example_plugin';

export class ConfigManager {
    constructor(config_name) {
        this.root = new example_plugin.KeyValues(config_name);

        if (!this.root.valid()) {
            throw new Error(`Failed to create config: ${config_name}`);
        }
    }

    close() {
        if (this.root) {
            this.root.close();
            this.root = null;
        }
    }

    createSection(section_name) {
        const section = new example_plugin.KeyValues(section_name);

        if (!section.valid()) {
            return false;
        }

        try {
            this.root.AddSubKey(section);
            return true;
        } catch (e) {
            section.close();
            return false;
        }
    }

    getSection(section_name) {
        const section = this.root.FindKey(section_name);

        if (section && section.valid()) {
            return section;
        }
        return null;
    }

    setValue(section_name, key, value) {
        const section = this.getSection(section_name);

        if (section) {
            try {
                section.SetString(key, value);
                return true;
            } finally {
                section.close();
            }
        }
        return false;
    }

    getValue(section_name, key, defaultValue = "") {
        const section = this.getSection(section_name);

        if (section) {
            try {
                return section.GetString(key, defaultValue);
            } finally {
                section.close();
            }
        }
        return defaultValue;
    }

}

// Usage with try-finally
const config = new ConfigManager("ServerConfig");
try {
    config.createSection("Server");
    config.setValue("Server", "hostname", "My Server");
    config.setValue("Server", "maxplayers", "32");

    config.createSection("Game");
    config.setValue("Game", "mode", "competitive");
} finally {
    config.close();  // Must explicitly cleanup!
}

Best Practices

  1. Always use try-finally - Wrap resource usage in try-finally blocks for guaranteed cleanup
  2. Call close() explicitly - JavaScript has no deterministic destructors, manual cleanup is required
  3. Clean up global objects in pluginEnd() - Critical: Explicitly clean up all global or module-level class instances before plugin unload to avoid crashes
  4. Don't rely on FinalizationRegistry - Finalization timing is unpredictable, always use close()
  5. Check valid() for safety - Especially after operations that might fail
  6. Respect ownership - Don't use objects after transferring ownership
  7. Use release() sparingly - Only when you need manual control
  8. Avoid mixing styles - Prefer class API over raw C-style functions
  9. Handle errors - Be prepared to catch errors from methods called on closed handles
  10. Check returned objects - Methods might return null on failure
  11. Use TypeScript - Type definitions help catch errors at development time

When NOT to Use Classes

Classes are designed for resources that need lifecycle management. Don't define classes for:

  • Stateless utility functions - Simple functions that don't manage resources
  • Functions that return primitive values - No need to wrap simple getters
  • One-off operations - Operations that don't maintain state
  • Global singletons - Resources that live for the entire program lifetime

For these cases, continue using the regular function-based API.

Troubleshooting

"No constructor matched" Error

If the arguments fit none of the class constructors, the constructor throws an Error that starts with "No constructor matched the arguments". If the create function returns the invalid value, the object is created but valid() returns false:

Handle Constructor Failure
import * as example_plugin from ':example_plugin';

try {
    const kv = new example_plugin.KeyValues("Config");
    // ... use kv ...
    kv.close();
} catch (e) {
    console.error(`Failed to create KeyValues: ${e.message}`);
    // Handle error - maybe retry or use default config
}

Using Closed or Released Objects

Attempting to use an object after calling close() or release() will throw an error:

Using Closed Handle
import * as example_plugin from ':example_plugin';

const kv = new example_plugin.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 example_plugin from ':example_plugin';

const kv = new example_plugin.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"
}

Dangling References

Be careful with non-owning references when the owner is destroyed:

Dangling Reference Problem
import * as example_plugin from ':example_plugin';

function getChildRef() {
    const parent = new example_plugin.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 example_plugin.KeyValues("Parent");
    try {
        const child = parent.FindKey("Child");  // Non-owning reference
        return child ? child.MakeCopy() : null;  // The owned copy outlives parent; the caller closes it
    } finally {
        parent.close();
    }
}

Crashes on Plugin Unload

If your plugin crashes when unloading:

  1. Check global objects - Ensure all global class instances are cleaned up in pluginEnd()
  2. Check module-level collections - Clear any Map, Set, or Array containing class instances
  3. Check instance variables - Clean up class instance variables in pluginEnd()
  4. Add explicit cleanup - Use close() on all long-lived objects before plugin unload
Fix Unload Crash
import * as example_plugin from ':example_plugin';

// Global objects that caused crashes
let g_config = null;
const g_cache = new Map();

export class MyPlugin extends Plugin {
    pluginStart() {
        g_config = new example_plugin.KeyValues("Config");
        g_cache.set("main", new example_plugin.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");
    }
}

See Also

  • Learn how to export your own classes
  • Learn about importing functions
  • Plugin manifest reference