Skip to content

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 })
        },
    },
})

Порядок:

  1. MutationStore успешно изменил backend.
  2. Local cache notifications инвалидирован/обновлён.
  3. Sync отправил opt-in событие.
  4. Другая вкладка проверила runtimeId, scope, version, buster и TTL.
  5. Её активный QueryStore обновился.

HTTP по-прежнему запускают stores, а не BroadcastChannel handler UI.

Все options

OptionTypeDefaultЧто делает
runtimeIQueryRuntimeContractRuntime, cache которого синхронизируется.
runtimeIdstringОбязательный id семейства runtime во вкладках.
scopestringЛогическая data partition сообщения.
channelNamestringquery-layer-syncИмя BroadcastChannel.
modeinvalidate | hydrateinvalidateПередавать identity или сериализованные data.
versionnumber1Версия протокола приложения.
busterstringОтделяет несовместимые cache/build сообщения.
ttlnumber60000Максимальный возраст входящего сообщения.
shouldBroadcastQuery(query) => booleanmeta.broadcast === trueAllowlist query.
allowRemoteClearbooleanfalseРазрешить другой вкладке очистить весь текущий runtime cache.
allowRemoteRemovebooleanfalseРазрешить удалённое удаление конкретной query.
clockIClocksystem clockВнедряемое время тестов.
idGeneratorIIdGeneratorruntime 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. Повторный вызов безопасен.