Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в JavaScript.

Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе классов.

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
import * as s2sdk from ':s2sdk';

// Создать дескриптор KeyValues вручную
const kv_handle = s2sdk.Kv1Create("MyConfig");

// Установить свойства, используя дескриптор
s2sdk.Kv1SetName(kv_handle, "ServerSettings");

// Найти подключ
const subkey_handle = s2sdk.Kv1FindKey(kv_handle, "Players");

// Легко забыть очистку!
s2sdk.Kv1Destroy(subkey_handle);
s2sdk.Kv1Destroy(kv_handle);

С классами тот же код становится намного чище:

С классами (стиль ООП)
import * as s2sdk from ':s2sdk';

// Создать с использованием конструктора класса
const kv = new s2sdk.KeyValues("MyConfig");

// Использовать интуитивные методы
kv.SetName("ServerSettings");

// Найти подключ - возвращает экземпляр KeyValues
const subkey = kv.FindKey("Players");

// Должны вручную очистить!
subkey.close();
kv.close();

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Безопасность типов - Лучшее автодополнение в IDE с определениями TypeScript
  • Меньше ошибок - Труднее перепутать дескрипторы
  • Автоматическая валидация - Методы проверяют действительность дескриптора перед вызовом
  • Идиоматичный API для JavaScript - Ощущается естественным для разработчиков на JavaScript

Важное ограничение:

Как работают классы

Когда плагин определяет классы в своем манифесте, Plugify автоматически генерирует обертки классов JavaScript, которые:

  1. Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
  2. Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
  3. Управляют жизненным циклом - Регистрируются в FinalizationRegistry для возможной очистки
  4. Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
  5. Валидируют дескрипторы - Автоматически проверяют действительность дескриптора перед вызовами методов
  6. Предоставляют подсказки типов - Генерируют .d.ts файлы определений TypeScript для поддержки IDE

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

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" }
    }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "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: Имя класса JavaScript (рекомендуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально)
  • bindings: Массив методов, доступных в классе
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код JavaScript

Когда вы определяете классы в своем манифесте, Plugify генерирует обертки классов JavaScript с несколькими встроенными методами:

Сгенерированный класс (концептуально)
class KeyValues {
    constructor(...args) {
        // Инициализировать в недопустимое состояние
        this._handle = 0;  // invalidValue
        this._owned = Ownership.BORROWED;

        // Конструкция прямого дескриптора
        // Паттерн: new KeyValues(handle_value, Ownership.OWNED)
        if (args.length >= 2 && Ownership.is(args[1])) {
            this._handle = args[0];
            this._owned = args[1];

            // Зарегистрировать для финализации, если принадлежит
            if (this._owned === Ownership.OWNED) {
                finalizerRegistry.register(this, {
                    handle: this._handle,
                    owned: this._owned,
                    destructor: _plugin.Kv1Destroy,
                    invalidValue: 0
                }, this);
            }
            return;
        }

        // Режим вызова конструктора
        try {
            this._handle = _plugin.Kv1Create(...args);
            this._owned = Ownership.OWNED;

            // Зарегистрировать для финализации
            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);
            // Отменить регистрацию в финализаторе
            finalizerRegistry.unregister(this);
        }
        this._handle = 0;
        this._owned = Ownership.BORROWED;
    }

    // Утилитарные методы
    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;
    }

    // Связанные методы (с автоматической валидацией дескриптора)
    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);
        // Автоматически оборачивает возвращаемое значение на основе 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");
        }
        // Автоматически освобождает владение от параметра subKey
        const handle = (typeof subKey.release === 'function') ? subKey.release() : subKey;
        _plugin.Kv1AddSubKey(this._handle, handle);
    }
}

Соответствующий .d.ts файл определений TypeScript также генерируется для поддержки IDE:

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

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

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

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

Встроенные утилитарные методы

Каждый сгенерированный класс включает несколько утилитарных методов для управления дескриптором:

valid() - Проверка действительности дескриптора

Возвращает true, если дескриптор действителен (не равен invalidValue):

Использование valid()
import * as s2sdk from ':s2sdk';

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

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

// После release дескриптор становится недействительным
const handle = kv.release();
console.log(kv.valid());  // false

get() - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование get()
import * as s2sdk from ':s2sdk';

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

// Получить необработанный дескриптор
const raw_handle = kv.get();
console.log(`Handle value: ${raw_handle}`);

// Передать в функцию, ожидающую необработанный дескриптор
some_c_function(kv.get());

release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:

Использование release()
import * as s2sdk from ':s2sdk';

function create_and_release() {
    const kv = new s2sdk.KeyValues("Config");
    kv.SetName("ServerConfig");

    // Передать владение наружу
    const handle = kv.release();

    // kv теперь недействителен, не будет очищать
    console.log(kv.valid());  // false

    return handle;
}

// Теперь мы владеем дескриптором и должны очистить его вручную
const raw_handle = create_and_release();
// ... использовать raw_handle ...
s2sdk.Kv1Destroy(raw_handle);  // Требуется ручная очистка!

Используйте release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

close() - Ручная очистка

Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:

Использование close()
import * as s2sdk from ':s2sdk';

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

// Явно закрыть дескриптор сейчас
kv.close();

// Дескриптор теперь недействителен
console.log(kv.valid());  // false

// Методы вызовут ошибку
try {
    kv.SetName("Test");
} catch (e) {
    console.error(`Error: ${e.message}`);  // "KeyValues handle is closed"
}

Метод close():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Отменяет регистрацию в финализаторе - Предотвращает двойную очистку
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором
  • Требуется для немедленной очистки - JavaScript не имеет детерминированных деструкторов

reset() - Псевдоним для Close

Удобный метод, который вызывает close():

Использование reset()
import * as s2sdk from ':s2sdk';

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

// Reset эквивалентен close
kv.reset();

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

Управление ресурсами

Классы JavaScript, сгенерированные Plugify, требуют ручного управления ресурсами:

Ручная очистка (требуется)

JavaScript не имеет детерминированного механизма очистки ресурсов. Вы должны явно вызывать close():

Ручная очистка
import * as s2sdk from ':s2sdk';

function processConfig() {
    const kv = new s2sdk.KeyValues("Config");
    kv.SetName("ServerConfig");

    try {
        // ... использовать kv ...
    } finally {
        // КРИТИЧНО: Всегда очищайте в блоке finally
        kv.close();
    }
}

processConfig();

Лучшая практика: Всегда используйте блоки try-finally для обеспечения очистки:

Паттерн Try-Finally
import * as s2sdk from ':s2sdk';

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

    try {
        kv.SetName("ServerConfig");
        kv.SetString("hostname", "My Server");
        // ... использовать kv ...
    } finally {
        kv.close();  // Гарантированная очистка даже при исключениях
    }
}

FinalizationRegistry (недетерминированный резерв)

Plugify использует JavaScript FinalizationRegistry внутренне для очистки ресурсов при сборке мусора объектов. Однако это недетерминировано и на него не следует полагаться:

Резервная финализация
import * as s2sdk from ':s2sdk';

function processConfig() {
    const kv = new s2sdk.KeyValues("Config");
    kv.SetName("ServerConfig");
    // ... использовать kv ...
    // НЕТ вызова close() - полагается на финализацию (ПЛОХО!)
}

processConfig();
// kv в конечном итоге будет очищен FinalizationRegistry
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:

Обертка без деструктора
import * as s2sdk from ':s2sdk';

// Класс без деструктора - просто удобная обертка
const wrapper = new s2sdk.SomeWrapper();

// Все еще имеет утилитарные методы
if (wrapper.valid()) {
    const handle = wrapper.get();
}

// Нет автоматической очистки - дескриптор сохраняется
// Полезно для оберток без состояния или глобальных ресурсов

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:

Передача владения
import * as s2sdk from ':s2sdk';

// Создать родителя и ребенка
const parent = new s2sdk.KeyValues("Parent");
const child = new s2sdk.KeyValues("Child");

try {
    // AddSubKey принимает владение child
    parent.AddSubKey(child);

    // child теперь принадлежит parent
    // child.valid() может все еще быть true, но не используйте его!
    // parent обработает очистку
} finally {
    parent.close();
}

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "subKey",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения избегайте использования объекта:

Правильная обработка владения
import * as s2sdk from ':s2sdk';

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

try {
    // Передать владение
    parent.AddSubKey(child);

    // Освободить нашу ссылку, чтобы предотвратить случайное использование
    child.release();  // Теперь child.valid() === false

    // Использовать только через parent
    const found = parent.FindKey("Child");
    if (found) {
        found.close();
    }
} finally {
    parent.close();
}

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
import * as s2sdk from ':s2sdk';

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

try {
    // FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
    const child = parent.FindKey("Settings");

    if (child && child.valid()) {
        try {
            child.SetName("UpdatedSettings");
            // Мы ответственны за жизненный цикл child
        } finally {
            child.close();  // Должны очистить возвращаемый объект
        }
    }
} finally {
    parent.close();
}

В манифесте:

Возврат владения в манифесте
{
  "name": "FindKey",
  "method": "Kv1FindKey",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
import * as s2sdk from ':s2sdk';

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

try {
    // GetFirstSubKey возвращает ссылку, parent все еще владеет ею
    const child_ref = parent.GetFirstSubKey();

    if (child_ref && child_ref.valid()) {
        // Используйте ссылку, но НЕ закрывайте ее
        const name = child_ref.GetName();
        // Не вызывайте child_ref.close() или child_ref.release()
        // child_ref будет очищен parent
    }
} finally {
    parent.close();
}

Жизненный цикл плагина и глобальные объекты

При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. Если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.

Управление глобальными объектами

Если вы храните экземпляры классов в глобальных переменных или объектах уровня модуля, вы должны явно очистить их в функции pluginEnd() вашего плагина:

plugin.mjs
import { Plugin } from 'plugify';
import * as s2sdk from ':s2sdk';

// Глобальный объект - опасен, если не очищен!
let g_config = null;

export class MyPlugin extends Plugin {
    pluginStart() {
        // Создать глобальную конфигурацию
        g_config = new s2sdk.KeyValues("GlobalConfig");
        g_config.SetName("ServerSettings");
        console.log("Plugin started with global config");
    }

    pluginEnd() {
        // КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
        if (g_config) {
            g_config.close();
            g_config = null;
        }
        console.log("Plugin ended, global config cleaned up");
    }
}

Безопасные паттерны

✅ Безопасно: Try-finally с очисткой

Безопасный паттерн
export class MyPlugin extends Plugin {
    onCommand(args) {
        const kv = new s2sdk.KeyValues("TempConfig");

        try {
            kv.SetName("CommandConfig");
            // ... использовать kv ...
        } finally {
            kv.close();  // Гарантированная очистка - безопасно!
        }
    }
}

✅ Безопасно: Переменная экземпляра с очисткой

Безопасный паттерн
export class MyPlugin extends Plugin {
    pluginStart() {
        this.config = new s2sdk.KeyValues("PluginConfig");
        this.config.SetName("Settings");
    }

    pluginEnd() {
        // Очистить переменные экземпляра
        if (this.config) {
            this.config.close();
            this.config = null;
        }
    }
}

❌ Небезопасно: Глобальная без очистки

Небезопасный паттерн
import * as s2sdk from ':s2sdk';

// ОПАСНО: Глобальный объект
const g_config = new s2sdk.KeyValues("GlobalConfig");

export class MyPlugin extends Plugin {
    pluginStart() {
        g_config.SetName("ServerSettings");
    }

    pluginEnd() {
        // ОТСУТСТВУЕТ: Нет очистки!
        // Финализатор g_config запустится после выгрузки плагина - СБОЙ!
    }
}

❌ Небезопасно: Кэш уровня модуля без очистки

Небезопасный паттерн
import * as s2sdk from ':s2sdk';

// ОПАСНО: Кэш уровня модуля
const kv_cache = new Map();

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

    pluginEnd() {
        // ОТСУТСТВУЕТ: Нет очистки кэша!
        // Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
    }
}

Контрольный список очистки

Перед выгрузкой вашего плагина (pluginEnd()), убедитесь:

  1. ✅ Все глобальные экземпляры классов явно закрыты или установлены в null
  2. ✅ Все коллекции уровня модуля (Map, Set, Array), содержащие экземпляры классов, очищены
  3. ✅ Все переменные экземпляра класса явно очищены
  4. ✅ Не осталось ссылок на экземпляры классов в долгоживущих структурах данных

Полный пример: Система конфигурации

Вот полный пример, показывающий, как использовать классы для системы конфигурации:

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

export class ConfigManager {
    constructor(config_name) {
        this.root = new s2sdk.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 s2sdk.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;
    }

    save(filename) {
        if (!this.root.valid()) {
            return false;
        }
        return this.root.SaveToFile(filename);
    }

    load(filename) {
        if (!this.root.valid()) {
            return false;
        }
        return this.root.LoadFromFile(filename);
    }
}

// Использование с 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");

    config.save("config.kv");
} finally {
    config.close();  // Должны явно очистить!
}

Лучшие практики

  1. Всегда используйте try-finally - Оборачивайте использование ресурсов в блоки try-finally для гарантированной очистки
  2. Вызывайте close() явно - JavaScript не имеет детерминированных деструкторов, требуется ручная очистка
  3. Очищайте глобальные объекты в pluginEnd() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
  4. Не полагайтесь на FinalizationRegistry - Время финализации непредсказуемо, всегда используйте close()
  5. Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
  6. Уважайте владение - Не используйте объекты после передачи владения
  7. Используйте release() экономно - Только когда вам нужен ручной контроль
  8. Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
  9. Обрабатывайте ошибки - Будьте готовы перехватывать ошибки от методов, вызванных на закрытых дескрипторах
  10. Проверяйте возвращаемые объекты - Методы могут возвращать null при неудаче
  11. Используйте TypeScript - Определения типов помогают находить ошибки во время разработки

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

Ошибка "Failed to create X"

Если конструктор вызывает ошибку, базовая функция создания вернула недопустимый дескриптор:

Сбой конструктора дескриптора
import * as s2sdk from ':s2sdk';

try {
    const kv = new s2sdk.KeyValues("Config");
    // ... использовать kv ...
    kv.close();
} catch (e) {
    console.error(`Failed to create KeyValues: ${e.message}`);
    // Обработать ошибку - возможно повторить или использовать конфигурацию по умолчанию
}

Использование закрытых или освобожденных объектов

Попытка использовать объект после вызова close() или release() вызовет ошибку:

Использование закрытого дескриптора
import * as s2sdk from ':s2sdk';

const kv = new s2sdk.KeyValues("Config");
kv.close();

// Это вызовет ошибку
try {
    kv.SetName("Test");
} catch (e) {
    console.error(e.message);  // "KeyValues handle is closed"
}

// Сначала проверьте valid(), чтобы избежать исключений
if (kv.valid()) {
    kv.SetName("Test");
} else {
    console.log("Handle is closed!");
}

Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:

Автоматическая валидация
import * as s2sdk from ':s2sdk';

const kv = new s2sdk.KeyValues("Config");
const handle = kv.release();

// Любой вызов метода завершится неудачей
try {
    const name = kv.GetName();
} catch (e) {
    console.error(e.message);  // "KeyValues handle is closed"
}

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:

Проблема висячей ссылки
import * as s2sdk from ':s2sdk';

function getChildRef() {
    const parent = new s2sdk.KeyValues("Parent");
    const child = parent.GetFirstSubKey();  // Невладеющая ссылка
    parent.close();
    return child;  // ПЛОХО: parent был уничтожен!
}

// Это опасно!
const child_ref = getChildRef();
// child_ref теперь указывает на уничтоженную память

// Лучший подход:
function getChildOwned() {
    const parent = new s2sdk.KeyValues("Parent");
    try {
        const child = parent.FindKey("Child");  // Возвращает принадлежащий экземпляр
        // Нужно держать parent живым или убедиться, что child используется правильно
        return child;  // Вызывающий должен закрыть и parent, и child
    } catch (e) {
        parent.close();
        throw e;
    }
}

Сбои при выгрузке плагина

Если ваш плагин падает при выгрузке:

  1. Проверьте глобальные объекты - Убедитесь, что все глобальные экземпляры классов очищены в pluginEnd()
  2. Проверьте коллекции уровня модуля - Очистите все Map, Set или Array, содержащие экземпляры классов
  3. Проверьте переменные экземпляра - Очистите переменные экземпляра класса в pluginEnd()
  4. Добавьте явную очистку - Используйте close() для всех долгоживущих объектов перед выгрузкой плагина
Исправление сбоя при выгрузке
import * as s2sdk from ':s2sdk';

// Глобальные объекты, которые вызывали сбои
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() {
        // Очистить глобальные объекты
        if (g_config) {
            g_config.close();
            g_config = null;
        }

        // Очистить кэшированные объекты
        for (const [key, kv] of g_cache) {
            kv.close();
        }
        g_cache.clear();

        console.log("All resources cleaned up safely");
    }
}

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина