Тема
Cross-tab sync
setupBroadcastSync() синхронизирует выбранные query между вкладками одного origin через BroadcastChannel.
Он не заменяет persistence:
- persistence переживает reload;
- sync сообщает уже открытым вкладкам об изменении сейчас.
Подключение
ts
import { setupBroadcastSync } from "@dubium/query-layer/sync"
import { runtime } from "./query-runtime"
export const sync = setupBroadcastSync({
// Runtime, cache которого слушаем.
runtime,
// Должен совпадать во вкладках одного приложения.
runtimeId: "customer-portal",
// Логическая data partition. Используйте безопасный session hash.
scope: "tenant-user-partition-hash",
// Вкладки с другим channelName не увидят события.
channelName: "portal-query-events",
// Передаём только identity query, не data payload.
mode: "invalidate",
// Старое сообщение через минуту игнорируется.
ttl: 60_000,
// Смена формата/сборки отделяет несовместимые сообщения.
version: 1,
buster: "portal:api-v1",
})В SSR или браузере без BroadcastChannel функция возвращает безопасный no-op controller.
Opt-in query
ts
const appScope = runtime.createParticipantScope({
participantId: "portal-app",
participantType: "application",
})
const notifications = appScope.query.createStore<Notification[]>({
queryKey: () => ["notifications", "list"],
queryFn: ({ signal }) => notificationsApi.list({ signal }),
meta: {
// Default sync policy отправляет события только таких query.
broadcast: true,
},
})Без meta.broadcast === true query не отправляется по default policy.
Режим invalidate
ts
mode: "invalidate"Вкладка A успешно обновила query. Она отправляет только key/identity. Вкладка B помечает свой query stale. Если у неё есть активный QueryStore, он сам выполнит API-запрос со своим auth/session lifecycle.
Плюсы:
- response data не передаётся через BroadcastChannel;
- меньше размер сообщения;
- каждая вкладка получает актуальный ответ от backend;
- безопасный default для большинства приложений.
Минус: backend может получить дополнительный GET из другой вкладки.
Режим hydrate
ts
mode: "hydrate"Вкладка A отправляет сериализованную query data, вкладка B hydrate-ит cache без немедленного HTTP.
Используйте только для классифицированных non-sensitive данных. В этом режиме payload проходит через BroadcastChannel и доступен JavaScript того же origin.
Всегда задавайте узкий фильтр:
ts
const sync = setupBroadcastSync({
runtime,
runtimeId: "customer-portal",
mode: "hydrate",
shouldBroadcastQuery: (query) => {
return query.meta?.broadcast === true && query.queryKey[0] === "public-dictionary"
},
})Mutation → sync → другая вкладка
ts
const markNotificationRead = appScope.mutation.createStore({
mutationKey: "notifications.read",
request: (notificationId: string, config) => {
return notificationsApi.markRead(notificationId, config)
},
options: {
// Scoped facade преобразует logical key в partitioned key runtime.
onSuccess: () => {
void appScope.query.invalidate({ queryKey: ["notifications", "list"] }, { refetchActive: true })
},
},
})Порядок:
- MutationStore успешно изменил backend.
- Local cache notifications инвалидирован/обновлён.
- Sync отправил opt-in событие.
- Другая вкладка проверила runtimeId, scope, version, buster и TTL.
- Её активный QueryStore обновился.
HTTP по-прежнему запускают stores, а не BroadcastChannel handler UI.
Все options
| Option | Type | Default | Что делает |
|---|---|---|---|
runtime | IQueryRuntimeContract | — | Runtime, cache которого синхронизируется. |
runtimeId | string | — | Обязательный id семейства runtime во вкладках. |
scope | string | — | Логическая data partition сообщения. |
channelName | string | query-layer-sync | Имя BroadcastChannel. |
mode | invalidate | hydrate | invalidate | Передавать identity или сериализованные data. |
version | number | 1 | Версия протокола приложения. |
buster | string | — | Отделяет несовместимые cache/build сообщения. |
ttl | number | 60000 | Максимальный возраст входящего сообщения. |
shouldBroadcastQuery | (query) => boolean | meta.broadcast === true | Allowlist query. |
allowRemoteClear | boolean | false | Разрешить другой вкладке очистить весь текущий runtime cache. |
allowRemoteRemove | boolean | false | Разрешить удалённое удаление конкретной query. |
clock | IClock | system clock | Внедряемое время тестов. |
idGenerator | IIdGenerator | runtime generator | Генератор sender/message ids. |
Оставляйте allowRemoteClear и allowRemoteRemove выключенными без явного административного сценария.
Проверка входящих сообщений
Плагин проверяет:
- форму payload;
runtimeId;scope;version;buster;- TTL и слишком далёкое будущее timestamp;
- sender id;
- duplicate message id.
Это защита протокола, но не security boundary. Любой скрипт того же origin может открыть BroadcastChannel. Не передавайте secrets.
Custom filter
ts
shouldBroadcastQuery: (query) => {
const allowedNamespace = query.queryKey[0] === "dictionary"
return query.meta?.broadcast === true && allowedNamespace
}Callback получает read-only queryKey, queryHash и копию meta.
Lifecycle
ts
// Сначала прекращаем cross-tab subscription.
sync.dispose()
// Затем освобождаем stores/runtime.
appScope.dispose()
await runtime.dispose()sync.dispose() закрывает channel, снимает cache subscription и очищает message dedup state. Повторный вызов безопасен.