Google Apps Script — Сам себе триггер в библиотеке
Статья Михаила Смирнова, написанная для канала t.me/google_sheets
Чат канала: t.me/google_spreadsheets_chat
Другие статьи Миши, написанные для нашего канала: t.me/google_sheets/1203
Здравствуйте, товарищи!
Сегодня мы посмотрим, как ловко создать триггер изнутри библиотеки на внутреннюю функцию библиотеки в Google Apps Script.

На фото Леонид Иванович Рогозов, советский врач-хирург, участник 6-й Советской антарктической экспедиции, в 1961 году сделавший сам себе операцию по поводу острого аппендицита.
Зачем оно нужно, кто-то и сходу поймёт, им достаточно посмотреть пару строчек кода (собственно, можете глянуть), а для остальных товарищей ниже небольшая предыстория.
Предыстория
Я написал библиотеку, которая что-то делает в с таблицами. Часть функциональности библиотеки работает несколько нестабильно: в большинстве случаев всё ок, но бывают ошибки, которые непонятно, как исправлять. Как известно у Гугла баги, это их ошибки, а я-то, как известно, красавчик и ни при чём.
Пришлось нагородить костыль: написал функцию, которая сбрасывает всё в рабочее состояние, повесил её на кнопку в меню. Но это кто-то должен заметить, что оно перестало работать, призвать начальника, а он кнопку нажмёт (от греха только ему эта кнопку доступна).
Короче, так себе решение проблемы с призывом начальника. Лучше было бы в начале выполнения создать time-based триггер, который через пару минут сам запустит лечебную функцию. Если всё ок, и ничего не упало, то триггер в конце выполнения удалим, а если упало, то триггер отработает и всё полечит.
Чтобы всю логику, которая есть вокруг создания и удаления триггера не тащить в проект, пусть живёт в библиотеке, рядом с теми функциями чьи последствия будет лечить. Вот тут-то и возник вопрос, как изнутри библиотеки создать триггер на функцию, которая тоже живёт внутри библиотеки?
Про библиотеки (Google Apps Script Library)
Вводная статья про библиотеки уже была – https://telegra.ph/Google-Apps-Script-Library--Biblioteki-v-Gugl-Skriptah-11-02
Про триггеры
Есть простые триггеры (simple triggers): достаточно назвать функцию правильным именем, и она будет в соответствующий момент выполняться.
Например:
function onOpen(e) {
Logger.log('Документ кто-то открыл');
}
При открытии таблицы (если проект привязан к таблице), функция отработает, в лог будет добавлено сообщение.
Такие триггеры в библиотеках бесполезны. Едем дальше.
Есть устанавливаемые триггеры (installable triggers), которые нужно руками создать. Нужно выбрать функцию, которая будет вызвана триггером и настройки триггера:

2. Жмём, чтобы создать новый
3. Выбираем функцию из проекта
4, 5, 6. Ставим триггер, пусть раз в час запускается
Он появился в списке:

Но тут есть неудобство и одна проблема:
- создали руками, а надо автоматом, из кода
- выбрать можно только функции из проекта, из библиотеки − нельзя
Создаём триггер программно
Проблему и неудобство решим одним махом. Создаём триггер, который сработает не раньше, чем через 1 минуту:
ScriptApp.newTrigger('testlib.heal')
.timeBased()
.after(1)
.create();
Если создавать триггер программно, можно указать функцию до которой надо добираться через точку: метод объекта, метод подобъекта объекта или ещё как спрятанную. В нашем случае − это функция библиотеки heal(), которая занимается лечением, к которой мы доступаемся testlib.heal.
Прятать функцию в библиотеке, добавляя _, (назвав heal_()) нельзя, триггер её не найдёт.
Тут ещё небольшой бонус: можно создавать одноразовые триггеры, которые сработают через заданное количество минут. Вручную тоже можно, но придётся указать точное время запуска.
— Ок, а как из библиотеки создать?
— Вот так, как выше, и создать. Название функции знаем, название библиотеки тоже − готово.
— А если библиотека импортировалась под другим именем?, — спросите вы.
— Смотрю, тебя не проведёшь! — скажу я.
Как из библиотеки узнать, как её обозвали?
Это сделать можно, сейчас расскажу. Есть важный момент: некоторые глобальные объекты проекта, который использует библиотеку, шарятся с библиотекой, другие − нет.
К счастью ScriptApp из тех, что шарятся.
Добавим в код нашей библиотеки, в самое начало следующее:
// This library's Script Id
const SCRIPTID_ = '1qYE4E5tQ7FwrgANl6CRZ5nXK896GFX1TVWsnzP1C9Z3-y0qzmma9PGHt';
// The name used the project that imported this library
const LIBSYMBOL_ = JSON.parse(ScriptApp.getResource('appsscript').getDataAsString())
.dependencies
.libraries
.find((lib) => lib.libraryId === SCRIPTID_)
.userSymbol;
SCRIPTID_ − это переменная, в которую мы положили Id библиотеки, по нему она подключается.
LIBSYMBOL_ − тут будет название библиотеки, которое назначено в проекте, который её импортировал.
Достаём его с помощью недокументированной (о, да!) функции ScriptApp.getResource('resourceName'), которая возвращает содержимое ресурса (одного из файлов проекта) в виде blob. resourceName − имя файла в проекте, без расширения. В нашем случае мы достаём содержимое манифеста appsscript.json, в котором содержатся настройки проекта, в том числе и данные про подключённые библиотеки:

Далее мы из blob получаем текст (getDataAsString()), парсим его (JSON.parse(...)) − получаем объект, в котором потом находим запись, соответствующую нашей библиотеке, и берём из неё параметр userSymbol.
Вот теперь можем не беспокоиться, что кто-то библиотеку импортирует под другим именем:
ScriptApp.newTrigger(`${LIBSYMBOL_}.heal`)
.timeBased()
.after(1)
.create();
Всё вместе
Что в итоге?
Код библиотеки (можно импортировать себе, поиграться, скопировать код):
// This library's Script Id
const SCRIPTID_ = '1qYE4E5tQ7FwrgANl6CRZ5nXK896GFX1TVWsnzP1C9Z3-y0qzmma9PGHt';
// The name used the project that imported this library
const LIBSYMBOL_ = JSON.parse(ScriptApp.getResource('appsscript').getDataAsString())
.dependencies
.libraries
.find((lib) => lib.libraryId === SCRIPTID_)
.userSymbol;
Logger.log(`This library is imported with the name "${LIBSYMBOL_}".`);
function createTrigger_() {
ScriptApp.newTrigger(`${LIBSYMBOL_}.heal`)
.timeBased()
.after(1)
.create();
}
function heal(e) {
// Find the trigger that fired this function in the list of all triggers
const thisTimeBasedTrigger = ScriptApp.getProjectTriggers()
.find((trigger) => trigger.getUniqueId() === e.triggerUid);
// If the trigger was found
if (thisTimeBasedTrigger != null) {
// Delete the trigger
ScriptApp.deleteTrigger(thisTimeBasedTrigger)
}
// Actual healing
Logger.log('Healing!');
}
function work() {
// Create trigger just in case
createTrigger_();
// Actual work
Logger.log('Working!');
// Emulate crash
throw Error('что-то пошло не так...');
}
Функцию work() мы будем вызывать из нашего проекта, который использует библиотеку. Она создаёт триггер, который сработает через минуту, изображает бурную деятельность (пишет в лог одну строчку), потом падает, выкинув ошибку.
Функция heal(e) в этом примере чуть хитрее, хоть и тут заглушка вместо реальной деятельности.
- Во-первых, она имеет параметр e − это объект события, который передаётся триггером.
- Во вторых, мы прибираемся за собой. Используем параметр e.triggerUid, который содержит Id триггера, который вызвал функцию, чтобы найти и удалить этот триггер. Триггер-то одноразовый, зачем ему висеть после того, как отработает?
Код в проекте, который использует библиотеку:
function useLib() {
superLib.work();
}
Просто запускаем функцию из библиотеки testlib, которую мы импортировали под именем superLib (оно ей больше подходит).
Попробуем запустить:

2. Это мы типа работаем
3. А это мы сломались
Триггер был создан:

Через минуту запустил нужную функцию:

По ходу функция удалила триггер, который её запустил:

Замечания
Пара слов про недокументированную функцию ScriptApp.getResource('...'). Я её вчера нашёл, разглядывая содержимое объектов: так Object.keys(ScriptApp) и так Object.getOwnPropertyNames(ScriptApp).
Что делает, не понял, загуглил. На Stackoverflow пишут (аж в 2012 году! куда милиция смотрит?!), что выдаёт содержимое файла проекта.
В сентябре в отличном чате по теме её находил Максим Стоянов, который использовал для чего-то другого, тоже очень полезного, я пока не разобрался. Максим выражал надежду, что скоро Гугл нам про неё расскажет, но оказывается прошло не меньше 9 лет, а они всё молчат. :)
Помимо того, чтобы библиотеке узнать, как её обозвали, функцию можно использовать, чтобы следить за тем, какие версии в каких проектах используются. Например, можно будет сразу ходить и обновлять настройки.
Попутно было замечено, что можно скопировать в проекте appscript.json, и у вас будет .json в проекте. Штатно можно создать только файл скрипта .gs или HTML код .html. Зачем вам .json в проекте, не знаю, но может пригодится?
Правда, .json этот должен иметь валидную структуру манифеста. Вроде бесполезная штука.

На этом всё. Спасибо за внимание.
Ссылки
Google Apps Script Library
- Про библиотеки
- Очень важно про то, какие части проекта, который использует библиотеку, видны в библиотеке
Триггеры
- Simple triggers и installable triggers
- newTrigger() и TriggerBuilder – создаём триггер программно
- Содержимое событий триггеров
Google Apps Script
- Про указание функций, до которых надо добираться через точку
- И вообще про используемый в скриптах движок V8
- Недокументированная функция ScripApp.getResource(): на Stackoverflow и от Максима Стоянова
- Про структуру appsscipt.json
Статья Михаила Смирнова, написанная для канала t.me/google_sheets
Чат канала: t.me/google_spreadsheets_chat
Другие статьи Миши, написанные для нашего канала: t.me/google_sheets/1203