Hooki – komunikacja luźno powiązana
System hooków to mechanizm wzorca publish-subscribe zaimplementowany w Orbito. Jeden moduł ogłasza zdarzenie (execHook), a wszystkie moduły, które się na nie zapisały (registerHook), otrzymują wywołanie i mogą zwrócić wynik. Wywołujący dostaje zebrane odpowiedzi od wszystkich nasłuchujących modułów.
To podejście pozwala rozszerzać zachowanie platformy bez modyfikacji istniejących modułów – wystarczy zainstalować nowy moduł, który rejestruje obsługę danego hooka.
Core.execHook – wywołanie zbiorcze
Wywołuje wszystkie moduły zarejestrowane na dany hook i zwraca tablicę ich odpowiedzi.
const results = await Core.execHook(hookName, data?)
| Parametr | Typ | Opis |
|---|---|---|
hookName | string | Nazwa hooka |
data | any | Opcjonalne dane przekazywane do każdego handlera |
Hook dosięga wyłącznie modułów tej samej firmy – platforma ustala kontekst na podstawie tożsamości połączenia, więc companyId nie jest przekazywane. Jeśli firma ma kilka instalacji tego samego modułu, hook trafia do każdej z nich, a odpowiedzi wszystkich instancji lądują w zwracanej tablicy.
Przykład – moduł zamówień zbiera dostępnych przewoźników:
const { Core, Log } = require('@orbito/api-lib');
Core.registerMethod('shipping.list', async (params, ctx) => {
// Wywołaj wszystkie moduły obsługujące przewoźników
const carriers = await Core.execHook(
'core.shipping.getCarrier',
{ order: params.order }
);
// carriers to tablica odpowiedzi od wszystkich zainstalowanych modułów
// Odfiltruj null (moduły, które nie obsługują danego zamówienia)
const available = carriers.filter(Boolean);
Log.info('Pobrano przewoźników', { count: available.length });
return available;
});
Core.registerHook – nasłuchiwanie na hook
Rejestruje moduł jako implementatora danego hooka. Wywoływany gdy inny moduł wyemituje ten hook przez execHook.
Core.registerHook(hookName, handler)
| Parametr | Typ | Opis |
|---|---|---|
hookName | string | Nazwa hooka, na który moduł reaguje |
handler | Function | Asynchroniczny handler |
Sygnatura handlera:
async (data) => { ... }
Handler dostaje dokładnie te argumenty, które wywołujący przekazał do execHook – nic więcej. Kontekst wywołania nie jest argumentem, bo liczbę argumentów hooka ustala wywołujący; trafia do handlera jako this (patrz Kto wywołał hook).
Przykład – moduł integracji DPD implementuje hook przewoźnika:
const { Core, Log } = require('@orbito/api-lib');
const ConfigSchema = require('./schemas/config');
Core.registerHook('core.shipping.getCarrier', async ({ order }) => {
// Pobierz konfigurację DPD (automatycznie w kontekście firmy modułu)
const config = await ConfigSchema.findOne({});
// Jeśli moduł nie jest skonfigurowany lub włączony, zwróć null
if (!config?.enabled) return null;
// Oblicz cenę na podstawie wagi paczki
const price = order.weight > 30 ? config.heavyRate : config.flatRate;
return {
carrierId: 'dpd',
label: 'DPD Kurier',
price,
estimatedDays: 1
};
});
Log.info('Moduł DPD załadowany');
Kto wywołał hook
Firma może mieć kilka instalacji tego samego modułu – np. trzy sklepy Prestashop, z których każdy zgłasza zamówienia tym samym hookiem. Odbiorca musi więc wiedzieć nie tylko co się stało, ale też gdzie.
Platforma dokłada do każdego wywołania tożsamość wywołującego. Nie jest ona argumentem handlera (liczba argumentów hooka należy do wywołującego) – trafia jako this:
Core.registerHook('event:order:created', async function (order) {
// this.caller = { instanceId, moduleCode, instanceName } albo null
if (!this.caller) {
// hook odpalony przez platformę (akcja użytkownika, automatyzacja)
return;
}
await WarehouseSchema.insert({
orderId: order._id,
sourceInstanceId: this.caller.instanceId, // z którego sklepu
sourceName: this.caller.instanceName // np. 'Sklep PL'
});
});
| Pole | Typ | Opis |
|---|---|---|
this.caller | object | null | null, gdy hook odpaliła platforma, a nie inny moduł |
caller.instanceId | string | Identyfikator instalacji, która odpaliła hook |
caller.moduleCode | string | Kod modułu wywołującego, np. core/prestashop |
caller.instanceName | string | null | Nazwa nadana instalacji przy instalacji |
Tożsamość jest budowana przez platformę z tożsamości połączenia, więc wywołujący nie może jej podrobić.
Kontekst jest opcjonalny
Handler, który nie potrzebuje caller, zostaje funkcją strzałkową i niczego nie zmienia. Kontekstu potrzebujesz → napisz zwykłą function.
Uwaga: Function.prototype.bind ma pierwszeństwo przed wiązaniem robionym przez SDK – handler zarejestrowany jako obj.handler.bind(obj) (albo pole klasy ze strzałką) dostanie this.caller === undefined.
Odpowiadanie konkretnej instancji
Jeśli w reakcji na hook chcesz oddzwonić do jego nadawcy, użyj Core.callInstanceMethod(this.caller.instanceId, ...) – Core.callModule po samym kodzie modułu jest niejednoznaczne, gdy firma ma kilka instalacji. Szczegóły w Core.callModule.
Konwencja nazewnictwa hooków
Hooki są identyfikowane przez string w formacie:
domena.kontekst.akcja
| Segment | Opis | Przykład |
|---|---|---|
domena | Moduł lub obszar systemu emitujący hook | core, shop, crm |
kontekst | Obszar tematyczny | shipping, payment, order |
akcja | Wykonywana operacja | getCarrier, beforeCreate, onComplete |
Przykłady nazw hooków:
core.shipping.getCarrier – pobranie dostępnych przewoźników
core.payment.getMethod – pobranie dostępnych metod płatności
core.order.beforeCreate – walidacja przed utworzeniem zamówienia
core.order.onComplete – akcje po finalizacji zamówienia
shop.product.afterImport – przetwarzanie po imporcie produktów
Hooki z prefiksem event: – przekazywanie na frontend
Hooki, których nazwa zaczyna się od event:, są objęte specjalną konwencją: platforma automatycznie przesyła je do podłączonych klientów frontendowych za pośrednictwem WebSocket. Dzięki temu komponenty UI mogą dynamicznie reagować na zmiany danych bez potrzeby odpytywania serwera.
await Core.execHook('event:order.update', { order });
await Core.execHook('event:product.delete', { productId });
Taki hook działa dwojako:
- po stronie backendowej wywołuje wszystkie zarejestrowane handlery (jak każdy inny hook),
- równocześnie rozsyła zdarzenie przez WebSocket do frontendów działających w kontekście danej firmy.
Przykład – aktualizacja zamówienia z powiadomieniem UI:
Core.registerMethod('order.update', async (params, ctx) => {
const order = await OrdersSchema.findOneAndUpdate(
{ _id: params.orderId },
{ $set: params.data },
{ new: true }
);
// Hook z prefiksem event: zostanie automatycznie przekazany na frontend
await Core.execHook('event:order.update', { order });
return { success: true };
});
Kiedy używać prefiksu event:
Stosuj event: dla hooków reprezentujących zmiany stanu encji (aktualizacja, usunięcie, zmiana statusu), na które frontend powinien reagować w czasie rzeczywistym – np. odświeżenie listy zamówień, ukrycie usuniętego produktu czy aktualizacja licznika.
Dokumentuj hooki swojego modułu
Jeśli Twój moduł definiuje własne hooki (wywołuje execHook), opisz je w dokumentacji modułu razem z oczekiwanym formatem data i zwracanego wyniku. Umożliwi to innym deweloperom tworzenie integracji z Twoim modułem.
Pełny przykład integracji
Poniżej kompletny scenariusz: moduł core/orders finalizuje zamówienie i ogłasza hook, a moduł integracji z systemem magazynowym nasłuchuje na to zdarzenie.
Moduł core/orders (emituje hook):
Core.registerMethod('order.complete', async (params, ctx) => {
const order = await OrdersSchema.findOne({ _id: params.orderId });
await OrdersSchema.update(
{ _id: order._id },
{ $set: { status: 'completed', completedAt: new Date() } }
);
// Powiadom wszystkie zainteresowane moduły o finalizacji
await Core.execHook('core.order.onComplete', { order });
return { success: true };
});
Moduł magazynu (obsługuje hook):
Core.registerHook('core.order.onComplete', async ({ order }) => {
Log.info('Aktualizacja stanu magazynowego po finalizacji', {
orderId: order._id
});
for (const item of order.cart) {
await WarehouseSchema.update(
{ sku: item.sku },
{ $inc: { stock: -item.quantity } }
);
}
return { updated: order.cart.length };
});
