Sign In

@sonenta/realtime

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sonenta/realtime - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+110
dist/chunk-4XBXVJX2.js
import {
LiveClient,
fetchCentrifugoToken,
fetchVersionMeta
} from "./chunk-2EOI7ZJZ.js";
// src/react/plugin.tsx
var DEFAULT_API_BASE = "https://api.sonenta.com";
function warn(msg, err) {
if (typeof console !== "undefined") {
if (err !== void 0) console.warn(`@sonenta/realtime: ${msg}`, err);
else console.warn(`@sonenta/realtime: ${msg}`);
}
}
function sonentaRealtime(options) {
return {
name: "@sonenta/realtime",
setup(ctx) {
const wsUrl = options.wsUrl;
if (!wsUrl) {
warn("wsUrl is empty; skipping subscription.");
return;
}
const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(
/\/+$/,
""
);
const projectUuid = ctx.config.projectUuid;
const apiToken = ctx.config.token;
const version = ctx.config.version ?? ctx.config.versionSlug ?? "main";
const tokenEndpoint = options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;
let cancelled = false;
let live = null;
void (async () => {
let meta;
try {
meta = await fetchVersionMeta(
apiBase,
projectUuid,
version,
apiToken
);
} catch (err) {
warn(`version metadata fetch failed for '${version}'`, err);
return;
}
if (cancelled) return;
if (!meta.is_dev || !meta.realtime_channel) {
warn(
`realtime is only available on dev versions; '${version}' is published \u2014 no subscription, serving static CDN bundles.`
);
return;
}
const channel = meta.realtime_channel;
const versionUuid = meta.uuid;
const refreshToken = async () => {
const { token: token2 } = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
return token2;
};
let token;
try {
const minted = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
token = minted.token;
} catch (err) {
warn("live token mint failed", err);
return;
}
if (cancelled) return;
live = new LiveClient({
url: wsUrl,
token,
channel,
refreshToken,
onMessage: (data) => {
if (!data || typeof data !== "object") return;
const d = data;
if (d.event !== "translations_published") return;
const lang = d.language_code;
const ns = d.namespace_slug;
if (!lang || !ns) return;
void ctx.i18n.reload({ locale: lang, namespace: ns });
}
});
live.connect();
})();
return () => {
cancelled = true;
if (live) {
live.dispose();
live = null;
}
};
}
};
}
export {
sonentaRealtime
};
//# sourceMappingURL=chunk-4XBXVJX2.js.map
{"version":3,"sources":["../src/react/plugin.tsx"],"sourcesContent":["/**\n * `@sonenta/realtime` as a PLUGIN of the `@sonenta/react-i18next` provider.\n * Adds zero-deploy translation updates for DEV versions: it reads the version's\n * dev/realtime state from the backend, and only for a dev version subscribes to\n * its Centrifugo channel and calls `ctx.i18n.reload(...)` on each\n * `translations_published` push.\n *\n * Dev-gating is PER-VERSION (backend contract #738), not based on the SDK's\n * `env` flag: a version is realtime-eligible until it has a published CDN\n * release. A published version returns `realtime_channel === null` → no\n * subscription, the bundle is served statically from the CDN.\n */\nimport { LiveClient, fetchCentrifugoToken } from \"../core/live\";\nimport { fetchVersionMeta } from \"../core/version\";\nimport type {\n SonentaPlugin,\n SonentaPluginContext,\n} from \"@sonenta/react-i18next\";\n\nexport interface SonentaRealtimeOptions {\n /**\n * WebSocket URL of the Centrifugo node, e.g.\n * `wss://rt.sonenta.dev/connection/websocket`. Required — the\n * plugin can't infer it from the API base.\n */\n wsUrl: string;\n /**\n * Endpoint that mints a short-lived Centrifugo connection token. Defaults to\n * `${apiBase}/v1/auth/centrifugo-token`. Override for self-hosted deployments.\n */\n tokenEndpoint?: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.sonenta.com\";\n\nfunction warn(msg: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n if (err !== undefined) console.warn(`@sonenta/realtime: ${msg}`, err);\n else console.warn(`@sonenta/realtime: ${msg}`);\n }\n}\n\n/**\n * Build the realtime plugin. Add it to `<SonentaProvider plugins={[...]}>`:\n *\n * ```tsx\n * plugins={[sonentaRealtime({ wsUrl: \"wss://centrifugo.example/connection/websocket\" })]}\n * ```\n */\nexport function sonentaRealtime(\n options: SonentaRealtimeOptions,\n): SonentaPlugin {\n return {\n name: \"@sonenta/realtime\",\n setup(ctx: SonentaPluginContext) {\n const wsUrl = options.wsUrl;\n if (!wsUrl) {\n warn(\"wsUrl is empty; skipping subscription.\");\n return;\n }\n\n const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n );\n const projectUuid = ctx.config.projectUuid;\n const apiToken = ctx.config.token;\n // Mirrors the core's version resolution (#739): `version` is the public\n // name, `versionSlug` a deprecated alias, default \"main\".\n const version = ctx.config.version ?? ctx.config.versionSlug ?? \"main\";\n const tokenEndpoint =\n options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;\n\n // Set by teardown; checked before opening the socket so an unmount during\n // the async setup never leaves a LiveClient connected post-unmount.\n let cancelled = false;\n let live: LiveClient | null = null;\n\n void (async () => {\n // 1. Read the version's dev/realtime state.\n let meta;\n try {\n meta = await fetchVersionMeta(\n apiBase,\n projectUuid,\n version,\n apiToken,\n );\n } catch (err) {\n warn(`version metadata fetch failed for '${version}'`, err);\n return;\n }\n if (cancelled) return;\n\n // 3. Published version → no realtime; serve static CDN bundles.\n if (!meta.is_dev || !meta.realtime_channel) {\n warn(\n `realtime is only available on dev versions; '${version}' is published — no subscription, serving static CDN bundles.`,\n );\n return;\n }\n const channel = meta.realtime_channel;\n const versionUuid = meta.uuid;\n\n // 2. Dev version → mint a version-scoped translations token + subscribe.\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n return token;\n };\n\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n token = minted.token;\n } catch (err) {\n warn(\"live token mint failed\", err);\n return;\n }\n if (cancelled) return; // teardown ran during the awaits — do not connect.\n\n live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => {\n if (!data || typeof data !== \"object\") return;\n const d = data as {\n event?: string;\n language_code?: string;\n namespace_slug?: string;\n };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n void ctx.i18n.reload({ locale: lang, namespace: ns });\n },\n });\n live.connect();\n })();\n\n // Teardown: cancel the in-flight setup and dispose any open client.\n return () => {\n cancelled = true;\n if (live) {\n live.dispose();\n live = null;\n }\n };\n },\n };\n}\n"],"mappings":";;;;;;;AAiCA,IAAM,mBAAmB;AAEzB,SAAS,KAAK,KAAa,KAAqB;AAC9C,MAAI,OAAO,YAAY,aAAa;AAClC,QAAI,QAAQ,OAAW,SAAQ,KAAK,sBAAsB,GAAG,IAAI,GAAG;AAAA,QAC/D,SAAQ,KAAK,sBAAsB,GAAG,EAAE;AAAA,EAC/C;AACF;AASO,SAAS,gBACd,SACe;AACf,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAA2B;AAC/B,YAAM,QAAQ,QAAQ;AACtB,UAAI,CAAC,OAAO;AACV,aAAK,wCAAwC;AAC7C;AAAA,MACF;AAEA,YAAM,WAAW,IAAI,OAAO,WAAW,kBAAkB;AAAA,QACvD;AAAA,QACA;AAAA,MACF;AACA,YAAM,cAAc,IAAI,OAAO;AAC/B,YAAM,WAAW,IAAI,OAAO;AAG5B,YAAM,UAAU,IAAI,OAAO,WAAW,IAAI,OAAO,eAAe;AAChE,YAAM,gBACJ,QAAQ,iBAAiB,GAAG,OAAO;AAIrC,UAAI,YAAY;AAChB,UAAI,OAA0B;AAE9B,YAAM,YAAY;AAEhB,YAAI;AACJ,YAAI;AACF,iBAAO,MAAM;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,eAAK,sCAAsC,OAAO,KAAK,GAAG;AAC1D;AAAA,QACF;AACA,YAAI,UAAW;AAGf,YAAI,CAAC,KAAK,UAAU,CAAC,KAAK,kBAAkB;AAC1C;AAAA,YACE,gDAAgD,OAAO;AAAA,UACzD;AACA;AAAA,QACF;AACA,cAAM,UAAU,KAAK;AACrB,cAAM,cAAc,KAAK;AAGzB,cAAM,eAAe,YAA6B;AAChD,gBAAM,EAAE,OAAAA,OAAM,IAAI,MAAM;AAAA,YACtB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,iBAAOA;AAAA,QACT;AAEA,YAAI;AACJ,YAAI;AACF,gBAAM,SAAS,MAAM;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,kBAAQ,OAAO;AAAA,QACjB,SAAS,KAAK;AACZ,eAAK,0BAA0B,GAAG;AAClC;AAAA,QACF;AACA,YAAI,UAAW;AAEf,eAAO,IAAI,WAAW;AAAA,UACpB,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,CAAC,SAAS;AACnB,gBAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,kBAAM,IAAI;AAKV,gBAAI,EAAE,UAAU,yBAA0B;AAC1C,kBAAM,OAAO,EAAE;AACf,kBAAM,KAAK,EAAE;AACb,gBAAI,CAAC,QAAQ,CAAC,GAAI;AAClB,iBAAK,IAAI,KAAK,OAAO,EAAE,QAAQ,MAAM,WAAW,GAAG,CAAC;AAAA,UACtD;AAAA,QACF,CAAC;AACD,aAAK,QAAQ;AAAA,MACf,GAAG;AAGH,aAAO,MAAM;AACX,oBAAY;AACZ,YAAI,MAAM;AACR,eAAK,QAAQ;AACb,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["token"]}
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/svelte/index.ts
var svelte_exports = {};
__export(svelte_exports, {
sonentaRealtime: () => sonentaRealtime
});
module.exports = __toCommonJS(svelte_exports);
// src/core/live.ts
var LiveClient = class {
constructor(cfg) {
this.cfg = cfg;
}
cfg;
_ws = null;
_id = 0;
_backoffMs = 1e3;
_disposed = false;
_connectAcked = false;
/** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
async connect() {
if (this._ws) return;
if (this._disposed) return;
this.cfg.onStatus?.("connecting");
const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
let url = this.cfg.url;
if (!url.includes("/connection/websocket")) {
url = url.replace(/\/+$/, "") + "/connection/websocket";
}
const ws = new WebSocket(url);
this._ws = ws;
this._connectAcked = false;
ws.onopen = () => {
this._send({ id: ++this._id, connect: { token } });
};
ws.onmessage = (evt) => this._onFrame(evt.data);
ws.onclose = () => this._onClose();
ws.onerror = () => {
};
}
dispose() {
this._disposed = true;
if (this._ws) {
try {
this._ws.close();
} catch {
}
this._ws = null;
}
this.cfg.onStatus?.("disconnected");
}
// ---- internals ----
_send(msg) {
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
try {
this._ws.send(JSON.stringify(msg));
} catch {
}
}
_onFrame(raw) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return;
}
if (!parsed || typeof parsed !== "object") return;
if (Object.keys(parsed).length === 0) {
this._send({});
return;
}
if (parsed.connect && !this._connectAcked) {
this._connectAcked = true;
this.cfg.onStatus?.("connected");
this._backoffMs = 1e3;
this._send({
id: ++this._id,
subscribe: { channel: this.cfg.channel }
});
return;
}
const push = parsed.push;
if (push && push.channel === this.cfg.channel && push.pub) {
this.cfg.onMessage(push.pub.data);
}
}
_onClose() {
this._ws = null;
this._connectAcked = false;
this.cfg.onStatus?.("disconnected");
if (this._disposed) return;
const delay = this._backoffMs;
this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
setTimeout(() => {
if (!this._disposed) void this.connect();
}, delay);
}
};
async function fetchCentrifugoToken(endpoint, projectUuid, authToken, opts = {}, fetchImpl = fetch) {
const body = { project_uuid: projectUuid };
if (opts.kind) body.kind = opts.kind;
if (opts.versionUuid) body.version_uuid = opts.versionUuid;
const r = await fetchImpl(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `ApiKey ${authToken}`
},
body: JSON.stringify(body)
});
if (!r.ok) {
throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
}
return await r.json();
}
// src/core/version.ts
async function fetchVersionMeta(apiBase, projectUuid, idOrSlug, authToken, fetchImpl = fetch) {
const url = `${apiBase.replace(/\/+$/, "")}/v1/projects/${projectUuid}/versions/${encodeURIComponent(
idOrSlug
)}`;
const r = await fetchImpl(url, {
method: "GET",
headers: { Authorization: `ApiKey ${authToken}` },
credentials: "omit"
});
if (!r.ok) {
throw new Error(`version metadata endpoint ${r.status}: ${await r.text()}`);
}
return await r.json();
}
// src/react/plugin.tsx
var DEFAULT_API_BASE = "https://api.sonenta.com";
function warn(msg, err) {
if (typeof console !== "undefined") {
if (err !== void 0) console.warn(`@sonenta/realtime: ${msg}`, err);
else console.warn(`@sonenta/realtime: ${msg}`);
}
}
function sonentaRealtime(options) {
return {
name: "@sonenta/realtime",
setup(ctx) {
const wsUrl = options.wsUrl;
if (!wsUrl) {
warn("wsUrl is empty; skipping subscription.");
return;
}
const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(
/\/+$/,
""
);
const projectUuid = ctx.config.projectUuid;
const apiToken = ctx.config.token;
const version = ctx.config.version ?? ctx.config.versionSlug ?? "main";
const tokenEndpoint = options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;
let cancelled = false;
let live = null;
void (async () => {
let meta;
try {
meta = await fetchVersionMeta(
apiBase,
projectUuid,
version,
apiToken
);
} catch (err) {
warn(`version metadata fetch failed for '${version}'`, err);
return;
}
if (cancelled) return;
if (!meta.is_dev || !meta.realtime_channel) {
warn(
`realtime is only available on dev versions; '${version}' is published \u2014 no subscription, serving static CDN bundles.`
);
return;
}
const channel = meta.realtime_channel;
const versionUuid = meta.uuid;
const refreshToken = async () => {
const { token: token2 } = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
return token2;
};
let token;
try {
const minted = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
token = minted.token;
} catch (err) {
warn("live token mint failed", err);
return;
}
if (cancelled) return;
live = new LiveClient({
url: wsUrl,
token,
channel,
refreshToken,
onMessage: (data) => {
if (!data || typeof data !== "object") return;
const d = data;
if (d.event !== "translations_published") return;
const lang = d.language_code;
const ns = d.namespace_slug;
if (!lang || !ns) return;
void ctx.i18n.reload({ locale: lang, namespace: ns });
}
});
live.connect();
})();
return () => {
cancelled = true;
if (live) {
live.dispose();
live = null;
}
};
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
sonentaRealtime
});
//# sourceMappingURL=index.cjs.map
{"version":3,"sources":["../../src/svelte/index.ts","../../src/core/live.ts","../../src/core/version.ts","../../src/react/plugin.tsx"],"sourcesContent":["/**\n * `@sonenta/realtime/svelte` — the realtime missing-key/live-update plugin for\n * the Sonenta svelte binding. The plugin is framework-agnostic (it hooks the\n * engine via the provider-plugin `setup()` lifecycle), so this is the same\n * `sonentaRealtime` plugin as `/react`; pass it to the binding's `plugins`:\n *\n * import { createSonentaI18n } from \"@sonenta/svelte-i18n\";\n * import { sonentaRealtime } from \"@sonenta/realtime/svelte\";\n * createSonentaI18n({ ...config, plugins: [sonentaRealtime({ wsUrl })] });\n */\nexport { sonentaRealtime } from \"../react/plugin\";\nexport type { SonentaRealtimeOptions } from \"../react/plugin\";\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Sonenta\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend\n * (`POST /v1/auth/centrifugo-token`). For version-scoped translations\n * realtime, pass `kind: \"translations\"` + the `versionUuid`; the backend\n * grants the version channel only while that version is dev (contract #738).\n * Response: `{token, channel, expires_at}`.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n opts: {\n kind?: \"translations\" | \"missing\" | \"feedback\";\n versionUuid?: string;\n } = {},\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const body: Record<string, unknown> = { project_uuid: projectUuid };\n if (opts.kind) body.kind = opts.kind;\n if (opts.versionUuid) body.version_uuid = opts.versionUuid;\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","/**\n * Version metadata used to gate dev-only realtime (backend contract #738).\n * The SDK-facing surface is intentionally just `is_dev` + `realtime_channel`\n * (plus `uuid`, needed for the version-scoped token); the rest is informative.\n */\nexport interface VersionMeta {\n uuid: string;\n project_uuid: string;\n name?: string;\n slug?: string;\n is_production?: boolean;\n /** true while the version has no published CDN release (= realtime-eligible). */\n is_dev: boolean;\n /** Centrifugo channel for dev versions; `null` once the version is published. */\n realtime_channel: string | null;\n}\n\n/**\n * `GET /v1/projects/{projectUuid}/versions/{idOrSlug}` — resolve a version by\n * uuid OR slug and read its dev/realtime state. Auth: project viewer (API key).\n */\nexport async function fetchVersionMeta(\n apiBase: string,\n projectUuid: string,\n idOrSlug: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<VersionMeta> {\n const url = `${apiBase.replace(/\\/+$/, \"\")}/v1/projects/${projectUuid}/versions/${encodeURIComponent(\n idOrSlug,\n )}`;\n const r = await fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${authToken}` },\n credentials: \"omit\",\n });\n if (!r.ok) {\n throw new Error(`version metadata endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as VersionMeta;\n}\n","/**\n * `@sonenta/realtime` as a PLUGIN of the `@sonenta/react-i18next` provider.\n * Adds zero-deploy translation updates for DEV versions: it reads the version's\n * dev/realtime state from the backend, and only for a dev version subscribes to\n * its Centrifugo channel and calls `ctx.i18n.reload(...)` on each\n * `translations_published` push.\n *\n * Dev-gating is PER-VERSION (backend contract #738), not based on the SDK's\n * `env` flag: a version is realtime-eligible until it has a published CDN\n * release. A published version returns `realtime_channel === null` → no\n * subscription, the bundle is served statically from the CDN.\n */\nimport { LiveClient, fetchCentrifugoToken } from \"../core/live\";\nimport { fetchVersionMeta } from \"../core/version\";\nimport type {\n SonentaPlugin,\n SonentaPluginContext,\n} from \"@sonenta/react-i18next\";\n\nexport interface SonentaRealtimeOptions {\n /**\n * WebSocket URL of the Centrifugo node, e.g.\n * `wss://rt.sonenta.dev/connection/websocket`. Required — the\n * plugin can't infer it from the API base.\n */\n wsUrl: string;\n /**\n * Endpoint that mints a short-lived Centrifugo connection token. Defaults to\n * `${apiBase}/v1/auth/centrifugo-token`. Override for self-hosted deployments.\n */\n tokenEndpoint?: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.sonenta.com\";\n\nfunction warn(msg: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n if (err !== undefined) console.warn(`@sonenta/realtime: ${msg}`, err);\n else console.warn(`@sonenta/realtime: ${msg}`);\n }\n}\n\n/**\n * Build the realtime plugin. Add it to `<SonentaProvider plugins={[...]}>`:\n *\n * ```tsx\n * plugins={[sonentaRealtime({ wsUrl: \"wss://centrifugo.example/connection/websocket\" })]}\n * ```\n */\nexport function sonentaRealtime(\n options: SonentaRealtimeOptions,\n): SonentaPlugin {\n return {\n name: \"@sonenta/realtime\",\n setup(ctx: SonentaPluginContext) {\n const wsUrl = options.wsUrl;\n if (!wsUrl) {\n warn(\"wsUrl is empty; skipping subscription.\");\n return;\n }\n\n const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n );\n const projectUuid = ctx.config.projectUuid;\n const apiToken = ctx.config.token;\n // Mirrors the core's version resolution (#739): `version` is the public\n // name, `versionSlug` a deprecated alias, default \"main\".\n const version = ctx.config.version ?? ctx.config.versionSlug ?? \"main\";\n const tokenEndpoint =\n options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;\n\n // Set by teardown; checked before opening the socket so an unmount during\n // the async setup never leaves a LiveClient connected post-unmount.\n let cancelled = false;\n let live: LiveClient | null = null;\n\n void (async () => {\n // 1. Read the version's dev/realtime state.\n let meta;\n try {\n meta = await fetchVersionMeta(\n apiBase,\n projectUuid,\n version,\n apiToken,\n );\n } catch (err) {\n warn(`version metadata fetch failed for '${version}'`, err);\n return;\n }\n if (cancelled) return;\n\n // 3. Published version → no realtime; serve static CDN bundles.\n if (!meta.is_dev || !meta.realtime_channel) {\n warn(\n `realtime is only available on dev versions; '${version}' is published — no subscription, serving static CDN bundles.`,\n );\n return;\n }\n const channel = meta.realtime_channel;\n const versionUuid = meta.uuid;\n\n // 2. Dev version → mint a version-scoped translations token + subscribe.\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n return token;\n };\n\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n token = minted.token;\n } catch (err) {\n warn(\"live token mint failed\", err);\n return;\n }\n if (cancelled) return; // teardown ran during the awaits — do not connect.\n\n live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => {\n if (!data || typeof data !== \"object\") return;\n const d = data as {\n event?: string;\n language_code?: string;\n namespace_slug?: string;\n };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n void ctx.i18n.reload({ locale: lang, namespace: ns });\n },\n });\n live.connect();\n })();\n\n // Teardown: cancel the in-flight setup and dispose any open client.\n return () => {\n cancelled = true;\n if (live) {\n live.dispose();\n live = null;\n }\n };\n },\n };\n}\n"],"mappings":"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","names":["token"]}
export { SonentaRealtimeOptions, sonentaRealtime } from '../react/index.cjs';
import '@sonenta/react-i18next';
export { SonentaRealtimeOptions, sonentaRealtime } from '../react/index.js';
import '@sonenta/react-i18next';
import {
sonentaRealtime
} from "../chunk-4XBXVJX2.js";
import "../chunk-2EOI7ZJZ.js";
export {
sonentaRealtime
};
//# sourceMappingURL=index.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/vue/index.ts
var vue_exports = {};
__export(vue_exports, {
sonentaRealtime: () => sonentaRealtime
});
module.exports = __toCommonJS(vue_exports);
// src/core/live.ts
var LiveClient = class {
constructor(cfg) {
this.cfg = cfg;
}
cfg;
_ws = null;
_id = 0;
_backoffMs = 1e3;
_disposed = false;
_connectAcked = false;
/** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */
async connect() {
if (this._ws) return;
if (this._disposed) return;
this.cfg.onStatus?.("connecting");
const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;
let url = this.cfg.url;
if (!url.includes("/connection/websocket")) {
url = url.replace(/\/+$/, "") + "/connection/websocket";
}
const ws = new WebSocket(url);
this._ws = ws;
this._connectAcked = false;
ws.onopen = () => {
this._send({ id: ++this._id, connect: { token } });
};
ws.onmessage = (evt) => this._onFrame(evt.data);
ws.onclose = () => this._onClose();
ws.onerror = () => {
};
}
dispose() {
this._disposed = true;
if (this._ws) {
try {
this._ws.close();
} catch {
}
this._ws = null;
}
this.cfg.onStatus?.("disconnected");
}
// ---- internals ----
_send(msg) {
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
try {
this._ws.send(JSON.stringify(msg));
} catch {
}
}
_onFrame(raw) {
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return;
}
if (!parsed || typeof parsed !== "object") return;
if (Object.keys(parsed).length === 0) {
this._send({});
return;
}
if (parsed.connect && !this._connectAcked) {
this._connectAcked = true;
this.cfg.onStatus?.("connected");
this._backoffMs = 1e3;
this._send({
id: ++this._id,
subscribe: { channel: this.cfg.channel }
});
return;
}
const push = parsed.push;
if (push && push.channel === this.cfg.channel && push.pub) {
this.cfg.onMessage(push.pub.data);
}
}
_onClose() {
this._ws = null;
this._connectAcked = false;
this.cfg.onStatus?.("disconnected");
if (this._disposed) return;
const delay = this._backoffMs;
this._backoffMs = Math.min(this._backoffMs * 2, 3e4);
setTimeout(() => {
if (!this._disposed) void this.connect();
}, delay);
}
};
async function fetchCentrifugoToken(endpoint, projectUuid, authToken, opts = {}, fetchImpl = fetch) {
const body = { project_uuid: projectUuid };
if (opts.kind) body.kind = opts.kind;
if (opts.versionUuid) body.version_uuid = opts.versionUuid;
const r = await fetchImpl(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `ApiKey ${authToken}`
},
body: JSON.stringify(body)
});
if (!r.ok) {
throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);
}
return await r.json();
}
// src/core/version.ts
async function fetchVersionMeta(apiBase, projectUuid, idOrSlug, authToken, fetchImpl = fetch) {
const url = `${apiBase.replace(/\/+$/, "")}/v1/projects/${projectUuid}/versions/${encodeURIComponent(
idOrSlug
)}`;
const r = await fetchImpl(url, {
method: "GET",
headers: { Authorization: `ApiKey ${authToken}` },
credentials: "omit"
});
if (!r.ok) {
throw new Error(`version metadata endpoint ${r.status}: ${await r.text()}`);
}
return await r.json();
}
// src/react/plugin.tsx
var DEFAULT_API_BASE = "https://api.sonenta.com";
function warn(msg, err) {
if (typeof console !== "undefined") {
if (err !== void 0) console.warn(`@sonenta/realtime: ${msg}`, err);
else console.warn(`@sonenta/realtime: ${msg}`);
}
}
function sonentaRealtime(options) {
return {
name: "@sonenta/realtime",
setup(ctx) {
const wsUrl = options.wsUrl;
if (!wsUrl) {
warn("wsUrl is empty; skipping subscription.");
return;
}
const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(
/\/+$/,
""
);
const projectUuid = ctx.config.projectUuid;
const apiToken = ctx.config.token;
const version = ctx.config.version ?? ctx.config.versionSlug ?? "main";
const tokenEndpoint = options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;
let cancelled = false;
let live = null;
void (async () => {
let meta;
try {
meta = await fetchVersionMeta(
apiBase,
projectUuid,
version,
apiToken
);
} catch (err) {
warn(`version metadata fetch failed for '${version}'`, err);
return;
}
if (cancelled) return;
if (!meta.is_dev || !meta.realtime_channel) {
warn(
`realtime is only available on dev versions; '${version}' is published \u2014 no subscription, serving static CDN bundles.`
);
return;
}
const channel = meta.realtime_channel;
const versionUuid = meta.uuid;
const refreshToken = async () => {
const { token: token2 } = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
return token2;
};
let token;
try {
const minted = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
token = minted.token;
} catch (err) {
warn("live token mint failed", err);
return;
}
if (cancelled) return;
live = new LiveClient({
url: wsUrl,
token,
channel,
refreshToken,
onMessage: (data) => {
if (!data || typeof data !== "object") return;
const d = data;
if (d.event !== "translations_published") return;
const lang = d.language_code;
const ns = d.namespace_slug;
if (!lang || !ns) return;
void ctx.i18n.reload({ locale: lang, namespace: ns });
}
});
live.connect();
})();
return () => {
cancelled = true;
if (live) {
live.dispose();
live = null;
}
};
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
sonentaRealtime
});
//# sourceMappingURL=index.cjs.map
{"version":3,"sources":["../../src/vue/index.ts","../../src/core/live.ts","../../src/core/version.ts","../../src/react/plugin.tsx"],"sourcesContent":["/**\n * `@sonenta/realtime/vue` — the realtime missing-key/live-update plugin for\n * the Sonenta vue binding. The plugin is framework-agnostic (it hooks the\n * engine via the provider-plugin `setup()` lifecycle), so this is the same\n * `sonentaRealtime` plugin as `/react`; pass it to the binding's `plugins`:\n *\n * import { createSonentaI18n } from \"@sonenta/vue-i18n\";\n * import { sonentaRealtime } from \"@sonenta/realtime/vue\";\n * createSonentaI18n({ ...config, plugins: [sonentaRealtime({ wsUrl })] });\n */\nexport { sonentaRealtime } from \"../react/plugin\";\nexport type { SonentaRealtimeOptions } from \"../react/plugin\";\n","/**\n * Tiny Centrifugo WebSocket client tailored to the Sonenta\n * `translations:` channel. Hand-rolled (no `centrifuge-js` dep) so the SDK\n * stays under 15 KB gzipped — we only need: connect, subscribe, listen.\n *\n * Wire format reference:\n * https://centrifugal.dev/docs/transports/websocket\n *\n * Lifecycle:\n * - call connect() with a valid token\n * - server replies with {push|reply}; we wait for the connect ack\n * - subscribe(channel) — send subscribe command, wait for ack\n * - subsequent {push, channel, pub.data} are routed to onMessage\n * - reconnect with exponential backoff (capped at 30s) on close\n */\n\nexport interface LiveClientConfig {\n url: string;\n token: string;\n channel: string;\n onMessage: (data: unknown) => void;\n onStatus?: (status: \"connecting\" | \"connected\" | \"disconnected\") => void;\n /**\n * Hook called just before each connect attempt — return a fresh token\n * (used to refresh a token whose `exp` is close). Defaults to the\n * static one passed in `token`.\n */\n refreshToken?: () => Promise<string>;\n}\n\nexport class LiveClient {\n private _ws: WebSocket | null = null;\n private _id = 0;\n private _backoffMs = 1_000;\n private _disposed = false;\n private _connectAcked = false;\n\n constructor(private readonly cfg: LiveClientConfig) {}\n\n /** Open the socket and try to subscribe. Idempotent — calling twice is a no-op. */\n async connect(): Promise<void> {\n if (this._ws) return;\n if (this._disposed) return;\n this.cfg.onStatus?.(\"connecting\");\n const token = this.cfg.refreshToken ? await this.cfg.refreshToken() : this.cfg.token;\n // Centrifugo's WebSocket endpoint lives at `/connection/websocket`.\n // Be forgiving on the input — accept either a bare host\n // (`wss://centrifugo.example`) or the full path.\n let url = this.cfg.url;\n if (!url.includes(\"/connection/websocket\")) {\n url = url.replace(/\\/+$/, \"\") + \"/connection/websocket\";\n }\n const ws = new WebSocket(url);\n this._ws = ws;\n this._connectAcked = false;\n\n ws.onopen = () => {\n // Centrifugo command: connect with token\n this._send({ id: ++this._id, connect: { token } });\n };\n ws.onmessage = (evt) => this._onFrame(evt.data);\n ws.onclose = () => this._onClose();\n ws.onerror = () => {\n // Let onclose handle the reconnect.\n };\n }\n\n dispose(): void {\n this._disposed = true;\n if (this._ws) {\n try {\n this._ws.close();\n } catch {\n // ignore\n }\n this._ws = null;\n }\n this.cfg.onStatus?.(\"disconnected\");\n }\n\n // ---- internals ----\n\n private _send(msg: unknown): void {\n if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n try {\n this._ws.send(JSON.stringify(msg));\n } catch {\n // ignore — onclose will fire shortly\n }\n }\n\n private _onFrame(raw: unknown): void {\n let parsed: {\n id?: number;\n connect?: { client?: string };\n subscribe?: unknown;\n push?: { channel?: string; pub?: { data?: unknown }; sub?: unknown };\n } | undefined;\n try {\n parsed = JSON.parse(raw as string);\n } catch {\n return;\n }\n if (!parsed || typeof parsed !== \"object\") return;\n // Centrifugo v2 protocol pings: server periodically sends an empty\n // object `{}`. Client MUST reply with an empty `{}` to keep the\n // connection alive — without this the server force-closes after\n // ~25-30s and we lose pushes silently.\n if (Object.keys(parsed).length === 0) {\n this._send({});\n return;\n }\n if (parsed.connect && !this._connectAcked) {\n this._connectAcked = true;\n this.cfg.onStatus?.(\"connected\");\n this._backoffMs = 1_000;\n // Connection token's `channels` claim auto-subscribes us server-side,\n // so an explicit subscribe command is unnecessary. We still send it\n // for back-compat with anonymous-channel deployments where no\n // server-side subscription was created.\n this._send({\n id: ++this._id,\n subscribe: { channel: this.cfg.channel },\n });\n return;\n }\n const push = parsed.push;\n if (push && push.channel === this.cfg.channel && push.pub) {\n this.cfg.onMessage(push.pub.data);\n }\n }\n\n private _onClose(): void {\n this._ws = null;\n this._connectAcked = false;\n this.cfg.onStatus?.(\"disconnected\");\n if (this._disposed) return;\n const delay = this._backoffMs;\n this._backoffMs = Math.min(this._backoffMs * 2, 30_000);\n setTimeout(() => {\n if (!this._disposed) void this.connect();\n }, delay);\n }\n}\n\n/**\n * Fetch a fresh Centrifugo connection token from the backend\n * (`POST /v1/auth/centrifugo-token`). For version-scoped translations\n * realtime, pass `kind: \"translations\"` + the `versionUuid`; the backend\n * grants the version channel only while that version is dev (contract #738).\n * Response: `{token, channel, expires_at}`.\n */\nexport async function fetchCentrifugoToken(\n endpoint: string,\n projectUuid: string,\n authToken: string,\n opts: {\n kind?: \"translations\" | \"missing\" | \"feedback\";\n versionUuid?: string;\n } = {},\n fetchImpl: typeof fetch = fetch,\n): Promise<{ token: string; channel: string; expires_at: number }> {\n const body: Record<string, unknown> = { project_uuid: projectUuid };\n if (opts.kind) body.kind = opts.kind;\n if (opts.versionUuid) body.version_uuid = opts.versionUuid;\n const r = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `ApiKey ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n if (!r.ok) {\n throw new Error(`centrifugo-token endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as { token: string; channel: string; expires_at: number };\n}\n","/**\n * Version metadata used to gate dev-only realtime (backend contract #738).\n * The SDK-facing surface is intentionally just `is_dev` + `realtime_channel`\n * (plus `uuid`, needed for the version-scoped token); the rest is informative.\n */\nexport interface VersionMeta {\n uuid: string;\n project_uuid: string;\n name?: string;\n slug?: string;\n is_production?: boolean;\n /** true while the version has no published CDN release (= realtime-eligible). */\n is_dev: boolean;\n /** Centrifugo channel for dev versions; `null` once the version is published. */\n realtime_channel: string | null;\n}\n\n/**\n * `GET /v1/projects/{projectUuid}/versions/{idOrSlug}` — resolve a version by\n * uuid OR slug and read its dev/realtime state. Auth: project viewer (API key).\n */\nexport async function fetchVersionMeta(\n apiBase: string,\n projectUuid: string,\n idOrSlug: string,\n authToken: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<VersionMeta> {\n const url = `${apiBase.replace(/\\/+$/, \"\")}/v1/projects/${projectUuid}/versions/${encodeURIComponent(\n idOrSlug,\n )}`;\n const r = await fetchImpl(url, {\n method: \"GET\",\n headers: { Authorization: `ApiKey ${authToken}` },\n credentials: \"omit\",\n });\n if (!r.ok) {\n throw new Error(`version metadata endpoint ${r.status}: ${await r.text()}`);\n }\n return (await r.json()) as VersionMeta;\n}\n","/**\n * `@sonenta/realtime` as a PLUGIN of the `@sonenta/react-i18next` provider.\n * Adds zero-deploy translation updates for DEV versions: it reads the version's\n * dev/realtime state from the backend, and only for a dev version subscribes to\n * its Centrifugo channel and calls `ctx.i18n.reload(...)` on each\n * `translations_published` push.\n *\n * Dev-gating is PER-VERSION (backend contract #738), not based on the SDK's\n * `env` flag: a version is realtime-eligible until it has a published CDN\n * release. A published version returns `realtime_channel === null` → no\n * subscription, the bundle is served statically from the CDN.\n */\nimport { LiveClient, fetchCentrifugoToken } from \"../core/live\";\nimport { fetchVersionMeta } from \"../core/version\";\nimport type {\n SonentaPlugin,\n SonentaPluginContext,\n} from \"@sonenta/react-i18next\";\n\nexport interface SonentaRealtimeOptions {\n /**\n * WebSocket URL of the Centrifugo node, e.g.\n * `wss://rt.sonenta.dev/connection/websocket`. Required — the\n * plugin can't infer it from the API base.\n */\n wsUrl: string;\n /**\n * Endpoint that mints a short-lived Centrifugo connection token. Defaults to\n * `${apiBase}/v1/auth/centrifugo-token`. Override for self-hosted deployments.\n */\n tokenEndpoint?: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.sonenta.com\";\n\nfunction warn(msg: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n if (err !== undefined) console.warn(`@sonenta/realtime: ${msg}`, err);\n else console.warn(`@sonenta/realtime: ${msg}`);\n }\n}\n\n/**\n * Build the realtime plugin. Add it to `<SonentaProvider plugins={[...]}>`:\n *\n * ```tsx\n * plugins={[sonentaRealtime({ wsUrl: \"wss://centrifugo.example/connection/websocket\" })]}\n * ```\n */\nexport function sonentaRealtime(\n options: SonentaRealtimeOptions,\n): SonentaPlugin {\n return {\n name: \"@sonenta/realtime\",\n setup(ctx: SonentaPluginContext) {\n const wsUrl = options.wsUrl;\n if (!wsUrl) {\n warn(\"wsUrl is empty; skipping subscription.\");\n return;\n }\n\n const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n );\n const projectUuid = ctx.config.projectUuid;\n const apiToken = ctx.config.token;\n // Mirrors the core's version resolution (#739): `version` is the public\n // name, `versionSlug` a deprecated alias, default \"main\".\n const version = ctx.config.version ?? ctx.config.versionSlug ?? \"main\";\n const tokenEndpoint =\n options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;\n\n // Set by teardown; checked before opening the socket so an unmount during\n // the async setup never leaves a LiveClient connected post-unmount.\n let cancelled = false;\n let live: LiveClient | null = null;\n\n void (async () => {\n // 1. Read the version's dev/realtime state.\n let meta;\n try {\n meta = await fetchVersionMeta(\n apiBase,\n projectUuid,\n version,\n apiToken,\n );\n } catch (err) {\n warn(`version metadata fetch failed for '${version}'`, err);\n return;\n }\n if (cancelled) return;\n\n // 3. Published version → no realtime; serve static CDN bundles.\n if (!meta.is_dev || !meta.realtime_channel) {\n warn(\n `realtime is only available on dev versions; '${version}' is published — no subscription, serving static CDN bundles.`,\n );\n return;\n }\n const channel = meta.realtime_channel;\n const versionUuid = meta.uuid;\n\n // 2. Dev version → mint a version-scoped translations token + subscribe.\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n return token;\n };\n\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n token = minted.token;\n } catch (err) {\n warn(\"live token mint failed\", err);\n return;\n }\n if (cancelled) return; // teardown ran during the awaits — do not connect.\n\n live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => {\n if (!data || typeof data !== \"object\") return;\n const d = data as {\n event?: string;\n language_code?: string;\n namespace_slug?: string;\n };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n void ctx.i18n.reload({ locale: lang, namespace: ns });\n },\n });\n live.connect();\n })();\n\n // Teardown: cancel the in-flight setup and dispose any open client.\n return () => {\n cancelled = true;\n if (live) {\n live.dispose();\n live = null;\n }\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC8BO,IAAM,aAAN,MAAiB;AAAA,EAOtB,YAA6B,KAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EANrB,MAAwB;AAAA,EACxB,MAAM;AAAA,EACN,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,gBAAgB;AAAA;AAAA,EAKxB,MAAM,UAAyB;AAC7B,QAAI,KAAK,IAAK;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,IAAI,WAAW,YAAY;AAChC,UAAM,QAAQ,KAAK,IAAI,eAAe,MAAM,KAAK,IAAI,aAAa,IAAI,KAAK,IAAI;AAI/E,QAAI,MAAM,KAAK,IAAI;AACnB,QAAI,CAAC,IAAI,SAAS,uBAAuB,GAAG;AAC1C,YAAM,IAAI,QAAQ,QAAQ,EAAE,IAAI;AAAA,IAClC;AACA,UAAM,KAAK,IAAI,UAAU,GAAG;AAC5B,SAAK,MAAM;AACX,SAAK,gBAAgB;AAErB,OAAG,SAAS,MAAM;AAEhB,WAAK,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,SAAS,EAAE,MAAM,EAAE,CAAC;AAAA,IACnD;AACA,OAAG,YAAY,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI;AAC9C,OAAG,UAAU,MAAM,KAAK,SAAS;AACjC,OAAG,UAAU,MAAM;AAAA,IAEnB;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,QAAI,KAAK,KAAK;AACZ,UAAI;AACF,aAAK,IAAI,MAAM;AAAA,MACjB,QAAQ;AAAA,MAER;AACA,WAAK,MAAM;AAAA,IACb;AACA,SAAK,IAAI,WAAW,cAAc;AAAA,EACpC;AAAA;AAAA,EAIQ,MAAM,KAAoB;AAChC,QAAI,CAAC,KAAK,OAAO,KAAK,IAAI,eAAe,UAAU,KAAM;AACzD,QAAI;AACF,WAAK,IAAI,KAAK,KAAK,UAAU,GAAG,CAAC;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,SAAS,KAAoB;AACnC,QAAI;AAMJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAa;AAAA,IACnC,QAAQ;AACN;AAAA,IACF;AACA,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAK3C,QAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,WAAK,MAAM,CAAC,CAAC;AACb;AAAA,IACF;AACA,QAAI,OAAO,WAAW,CAAC,KAAK,eAAe;AACzC,WAAK,gBAAgB;AACrB,WAAK,IAAI,WAAW,WAAW;AAC/B,WAAK,aAAa;AAKlB,WAAK,MAAM;AAAA,QACT,IAAI,EAAE,KAAK;AAAA,QACX,WAAW,EAAE,SAAS,KAAK,IAAI,QAAQ;AAAA,MACzC,CAAC;AACD;AAAA,IACF;AACA,UAAM,OAAO,OAAO;AACpB,QAAI,QAAQ,KAAK,YAAY,KAAK,IAAI,WAAW,KAAK,KAAK;AACzD,WAAK,IAAI,UAAU,KAAK,IAAI,IAAI;AAAA,IAClC;AAAA,EACF;AAAA,EAEQ,WAAiB;AACvB,SAAK,MAAM;AACX,SAAK,gBAAgB;AACrB,SAAK,IAAI,WAAW,cAAc;AAClC,QAAI,KAAK,UAAW;AACpB,UAAM,QAAQ,KAAK;AACnB,SAAK,aAAa,KAAK,IAAI,KAAK,aAAa,GAAG,GAAM;AACtD,eAAW,MAAM;AACf,UAAI,CAAC,KAAK,UAAW,MAAK,KAAK,QAAQ;AAAA,IACzC,GAAG,KAAK;AAAA,EACV;AACF;AASA,eAAsB,qBACpB,UACA,aACA,WACA,OAGI,CAAC,GACL,YAA0B,OACuC;AACjE,QAAM,OAAgC,EAAE,cAAc,YAAY;AAClE,MAAI,KAAK,KAAM,MAAK,OAAO,KAAK;AAChC,MAAI,KAAK,YAAa,MAAK,eAAe,KAAK;AAC/C,QAAM,IAAI,MAAM,UAAU,UAAU;AAAA,IAClC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,SAAS;AAAA,IACpC;AAAA,IACA,MAAM,KAAK,UAAU,IAAI;AAAA,EAC3B,CAAC;AACD,MAAI,CAAC,EAAE,IAAI;AACT,UAAM,IAAI,MAAM,6BAA6B,EAAE,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,EAAE,KAAK;AACvB;;;AC5JA,eAAsB,iBACpB,SACA,aACA,UACA,WACA,YAA0B,OACJ;AACtB,QAAM,MAAM,GAAG,QAAQ,QAAQ,QAAQ,EAAE,CAAC,gBAAgB,WAAW,aAAa;AAAA,IAChF;AAAA,EACF,CAAC;AACD,QAAM,IAAI,MAAM,UAAU,KAAK;AAAA,IAC7B,QAAQ;AAAA,IACR,SAAS,EAAE,eAAe,UAAU,SAAS,GAAG;AAAA,IAChD,aAAa;AAAA,EACf,CAAC;AACD,MAAI,CAAC,EAAE,IAAI;AACT,UAAM,IAAI,MAAM,6BAA6B,EAAE,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,EAAE,KAAK;AACvB;;;ACPA,IAAM,mBAAmB;AAEzB,SAAS,KAAK,KAAa,KAAqB;AAC9C,MAAI,OAAO,YAAY,aAAa;AAClC,QAAI,QAAQ,OAAW,SAAQ,KAAK,sBAAsB,GAAG,IAAI,GAAG;AAAA,QAC/D,SAAQ,KAAK,sBAAsB,GAAG,EAAE;AAAA,EAC/C;AACF;AASO,SAAS,gBACd,SACe;AACf,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAA2B;AAC/B,YAAM,QAAQ,QAAQ;AACtB,UAAI,CAAC,OAAO;AACV,aAAK,wCAAwC;AAC7C;AAAA,MACF;AAEA,YAAM,WAAW,IAAI,OAAO,WAAW,kBAAkB;AAAA,QACvD;AAAA,QACA;AAAA,MACF;AACA,YAAM,cAAc,IAAI,OAAO;AAC/B,YAAM,WAAW,IAAI,OAAO;AAG5B,YAAM,UAAU,IAAI,OAAO,WAAW,IAAI,OAAO,eAAe;AAChE,YAAM,gBACJ,QAAQ,iBAAiB,GAAG,OAAO;AAIrC,UAAI,YAAY;AAChB,UAAI,OAA0B;AAE9B,YAAM,YAAY;AAEhB,YAAI;AACJ,YAAI;AACF,iBAAO,MAAM;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,eAAK,sCAAsC,OAAO,KAAK,GAAG;AAC1D;AAAA,QACF;AACA,YAAI,UAAW;AAGf,YAAI,CAAC,KAAK,UAAU,CAAC,KAAK,kBAAkB;AAC1C;AAAA,YACE,gDAAgD,OAAO;AAAA,UACzD;AACA;AAAA,QACF;AACA,cAAM,UAAU,KAAK;AACrB,cAAM,cAAc,KAAK;AAGzB,cAAM,eAAe,YAA6B;AAChD,gBAAM,EAAE,OAAAA,OAAM,IAAI,MAAM;AAAA,YACtB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,iBAAOA;AAAA,QACT;AAEA,YAAI;AACJ,YAAI;AACF,gBAAM,SAAS,MAAM;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,kBAAQ,OAAO;AAAA,QACjB,SAAS,KAAK;AACZ,eAAK,0BAA0B,GAAG;AAClC;AAAA,QACF;AACA,YAAI,UAAW;AAEf,eAAO,IAAI,WAAW;AAAA,UACpB,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,CAAC,SAAS;AACnB,gBAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,kBAAM,IAAI;AAKV,gBAAI,EAAE,UAAU,yBAA0B;AAC1C,kBAAM,OAAO,EAAE;AACf,kBAAM,KAAK,EAAE;AACb,gBAAI,CAAC,QAAQ,CAAC,GAAI;AAClB,iBAAK,IAAI,KAAK,OAAO,EAAE,QAAQ,MAAM,WAAW,GAAG,CAAC;AAAA,UACtD;AAAA,QACF,CAAC;AACD,aAAK,QAAQ;AAAA,MACf,GAAG;AAGH,aAAO,MAAM;AACX,oBAAY;AACZ,YAAI,MAAM;AACR,eAAK,QAAQ;AACb,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["token"]}
export { SonentaRealtimeOptions, sonentaRealtime } from '../react/index.cjs';
import '@sonenta/react-i18next';
export { SonentaRealtimeOptions, sonentaRealtime } from '../react/index.js';
import '@sonenta/react-i18next';
import {
sonentaRealtime
} from "../chunk-4XBXVJX2.js";
import "../chunk-2EOI7ZJZ.js";
export {
sonentaRealtime
};
//# sourceMappingURL=index.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
export * from "./dist/svelte/index";
export * from "./dist/svelte/index.js";
export * from "./dist/vue/index";
export * from "./dist/vue/index.js";
+3
-104
import {
LiveClient,
fetchCentrifugoToken,
fetchVersionMeta
} from "../chunk-2EOI7ZJZ.js";
// src/react/plugin.tsx
var DEFAULT_API_BASE = "https://api.sonenta.com";
function warn(msg, err) {
if (typeof console !== "undefined") {
if (err !== void 0) console.warn(`@sonenta/realtime: ${msg}`, err);
else console.warn(`@sonenta/realtime: ${msg}`);
}
}
function sonentaRealtime(options) {
return {
name: "@sonenta/realtime",
setup(ctx) {
const wsUrl = options.wsUrl;
if (!wsUrl) {
warn("wsUrl is empty; skipping subscription.");
return;
}
const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(
/\/+$/,
""
);
const projectUuid = ctx.config.projectUuid;
const apiToken = ctx.config.token;
const version = ctx.config.version ?? ctx.config.versionSlug ?? "main";
const tokenEndpoint = options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;
let cancelled = false;
let live = null;
void (async () => {
let meta;
try {
meta = await fetchVersionMeta(
apiBase,
projectUuid,
version,
apiToken
);
} catch (err) {
warn(`version metadata fetch failed for '${version}'`, err);
return;
}
if (cancelled) return;
if (!meta.is_dev || !meta.realtime_channel) {
warn(
`realtime is only available on dev versions; '${version}' is published \u2014 no subscription, serving static CDN bundles.`
);
return;
}
const channel = meta.realtime_channel;
const versionUuid = meta.uuid;
const refreshToken = async () => {
const { token: token2 } = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
return token2;
};
let token;
try {
const minted = await fetchCentrifugoToken(
tokenEndpoint,
projectUuid,
apiToken,
{ kind: "translations", versionUuid }
);
token = minted.token;
} catch (err) {
warn("live token mint failed", err);
return;
}
if (cancelled) return;
live = new LiveClient({
url: wsUrl,
token,
channel,
refreshToken,
onMessage: (data) => {
if (!data || typeof data !== "object") return;
const d = data;
if (d.event !== "translations_published") return;
const lang = d.language_code;
const ns = d.namespace_slug;
if (!lang || !ns) return;
void ctx.i18n.reload({ locale: lang, namespace: ns });
}
});
live.connect();
})();
return () => {
cancelled = true;
if (live) {
live.dispose();
live = null;
}
};
}
};
}
sonentaRealtime
} from "../chunk-4XBXVJX2.js";
import "../chunk-2EOI7ZJZ.js";
export {

@@ -107,0 +6,0 @@ sonentaRealtime,

@@ -1,1 +0,1 @@

{"version":3,"sources":["../../src/react/plugin.tsx"],"sourcesContent":["/**\n * `@sonenta/realtime` as a PLUGIN of the `@sonenta/react-i18next` provider.\n * Adds zero-deploy translation updates for DEV versions: it reads the version's\n * dev/realtime state from the backend, and only for a dev version subscribes to\n * its Centrifugo channel and calls `ctx.i18n.reload(...)` on each\n * `translations_published` push.\n *\n * Dev-gating is PER-VERSION (backend contract #738), not based on the SDK's\n * `env` flag: a version is realtime-eligible until it has a published CDN\n * release. A published version returns `realtime_channel === null` → no\n * subscription, the bundle is served statically from the CDN.\n */\nimport { LiveClient, fetchCentrifugoToken } from \"../core/live\";\nimport { fetchVersionMeta } from \"../core/version\";\nimport type {\n SonentaPlugin,\n SonentaPluginContext,\n} from \"@sonenta/react-i18next\";\n\nexport interface SonentaRealtimeOptions {\n /**\n * WebSocket URL of the Centrifugo node, e.g.\n * `wss://rt.sonenta.dev/connection/websocket`. Required — the\n * plugin can't infer it from the API base.\n */\n wsUrl: string;\n /**\n * Endpoint that mints a short-lived Centrifugo connection token. Defaults to\n * `${apiBase}/v1/auth/centrifugo-token`. Override for self-hosted deployments.\n */\n tokenEndpoint?: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.sonenta.com\";\n\nfunction warn(msg: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n if (err !== undefined) console.warn(`@sonenta/realtime: ${msg}`, err);\n else console.warn(`@sonenta/realtime: ${msg}`);\n }\n}\n\n/**\n * Build the realtime plugin. Add it to `<SonentaProvider plugins={[...]}>`:\n *\n * ```tsx\n * plugins={[sonentaRealtime({ wsUrl: \"wss://centrifugo.example/connection/websocket\" })]}\n * ```\n */\nexport function sonentaRealtime(\n options: SonentaRealtimeOptions,\n): SonentaPlugin {\n return {\n name: \"@sonenta/realtime\",\n setup(ctx: SonentaPluginContext) {\n const wsUrl = options.wsUrl;\n if (!wsUrl) {\n warn(\"wsUrl is empty; skipping subscription.\");\n return;\n }\n\n const apiBase = (ctx.config.apiBase ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n );\n const projectUuid = ctx.config.projectUuid;\n const apiToken = ctx.config.token;\n // Mirrors the core's version resolution (#739): `version` is the public\n // name, `versionSlug` a deprecated alias, default \"main\".\n const version = ctx.config.version ?? ctx.config.versionSlug ?? \"main\";\n const tokenEndpoint =\n options.tokenEndpoint ?? `${apiBase}/v1/auth/centrifugo-token`;\n\n // Set by teardown; checked before opening the socket so an unmount during\n // the async setup never leaves a LiveClient connected post-unmount.\n let cancelled = false;\n let live: LiveClient | null = null;\n\n void (async () => {\n // 1. Read the version's dev/realtime state.\n let meta;\n try {\n meta = await fetchVersionMeta(\n apiBase,\n projectUuid,\n version,\n apiToken,\n );\n } catch (err) {\n warn(`version metadata fetch failed for '${version}'`, err);\n return;\n }\n if (cancelled) return;\n\n // 3. Published version → no realtime; serve static CDN bundles.\n if (!meta.is_dev || !meta.realtime_channel) {\n warn(\n `realtime is only available on dev versions; '${version}' is published — no subscription, serving static CDN bundles.`,\n );\n return;\n }\n const channel = meta.realtime_channel;\n const versionUuid = meta.uuid;\n\n // 2. Dev version → mint a version-scoped translations token + subscribe.\n const refreshToken = async (): Promise<string> => {\n const { token } = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n return token;\n };\n\n let token: string;\n try {\n const minted = await fetchCentrifugoToken(\n tokenEndpoint,\n projectUuid,\n apiToken,\n { kind: \"translations\", versionUuid },\n );\n token = minted.token;\n } catch (err) {\n warn(\"live token mint failed\", err);\n return;\n }\n if (cancelled) return; // teardown ran during the awaits — do not connect.\n\n live = new LiveClient({\n url: wsUrl,\n token,\n channel,\n refreshToken,\n onMessage: (data) => {\n if (!data || typeof data !== \"object\") return;\n const d = data as {\n event?: string;\n language_code?: string;\n namespace_slug?: string;\n };\n if (d.event !== \"translations_published\") return;\n const lang = d.language_code;\n const ns = d.namespace_slug;\n if (!lang || !ns) return;\n void ctx.i18n.reload({ locale: lang, namespace: ns });\n },\n });\n live.connect();\n })();\n\n // Teardown: cancel the in-flight setup and dispose any open client.\n return () => {\n cancelled = true;\n if (live) {\n live.dispose();\n live = null;\n }\n };\n },\n };\n}\n"],"mappings":";;;;;;;AAiCA,IAAM,mBAAmB;AAEzB,SAAS,KAAK,KAAa,KAAqB;AAC9C,MAAI,OAAO,YAAY,aAAa;AAClC,QAAI,QAAQ,OAAW,SAAQ,KAAK,sBAAsB,GAAG,IAAI,GAAG;AAAA,QAC/D,SAAQ,KAAK,sBAAsB,GAAG,EAAE;AAAA,EAC/C;AACF;AASO,SAAS,gBACd,SACe;AACf,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAA2B;AAC/B,YAAM,QAAQ,QAAQ;AACtB,UAAI,CAAC,OAAO;AACV,aAAK,wCAAwC;AAC7C;AAAA,MACF;AAEA,YAAM,WAAW,IAAI,OAAO,WAAW,kBAAkB;AAAA,QACvD;AAAA,QACA;AAAA,MACF;AACA,YAAM,cAAc,IAAI,OAAO;AAC/B,YAAM,WAAW,IAAI,OAAO;AAG5B,YAAM,UAAU,IAAI,OAAO,WAAW,IAAI,OAAO,eAAe;AAChE,YAAM,gBACJ,QAAQ,iBAAiB,GAAG,OAAO;AAIrC,UAAI,YAAY;AAChB,UAAI,OAA0B;AAE9B,YAAM,YAAY;AAEhB,YAAI;AACJ,YAAI;AACF,iBAAO,MAAM;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,eAAK,sCAAsC,OAAO,KAAK,GAAG;AAC1D;AAAA,QACF;AACA,YAAI,UAAW;AAGf,YAAI,CAAC,KAAK,UAAU,CAAC,KAAK,kBAAkB;AAC1C;AAAA,YACE,gDAAgD,OAAO;AAAA,UACzD;AACA;AAAA,QACF;AACA,cAAM,UAAU,KAAK;AACrB,cAAM,cAAc,KAAK;AAGzB,cAAM,eAAe,YAA6B;AAChD,gBAAM,EAAE,OAAAA,OAAM,IAAI,MAAM;AAAA,YACtB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,iBAAOA;AAAA,QACT;AAEA,YAAI;AACJ,YAAI;AACF,gBAAM,SAAS,MAAM;AAAA,YACnB;AAAA,YACA;AAAA,YACA;AAAA,YACA,EAAE,MAAM,gBAAgB,YAAY;AAAA,UACtC;AACA,kBAAQ,OAAO;AAAA,QACjB,SAAS,KAAK;AACZ,eAAK,0BAA0B,GAAG;AAClC;AAAA,QACF;AACA,YAAI,UAAW;AAEf,eAAO,IAAI,WAAW;AAAA,UACpB,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,CAAC,SAAS;AACnB,gBAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,kBAAM,IAAI;AAKV,gBAAI,EAAE,UAAU,yBAA0B;AAC1C,kBAAM,OAAO,EAAE;AACf,kBAAM,KAAK,EAAE;AACb,gBAAI,CAAC,QAAQ,CAAC,GAAI;AAClB,iBAAK,IAAI,KAAK,OAAO,EAAE,QAAQ,MAAM,WAAW,GAAG,CAAC;AAAA,UACtD;AAAA,QACF,CAAC;AACD,aAAK,QAAQ;AAAA,MACf,GAAG;AAGH,aAAO,MAAM;AACX,oBAAY;AACZ,YAAI,MAAM;AACR,eAAK,QAAQ;AACb,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["token"]}
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
{
"name": "@sonenta/realtime",
"version": "1.1.0",
"version": "1.2.0",
"description": "Realtime translation updates plugin for @sonenta/react-i18next (Centrifugo).",

@@ -32,2 +32,12 @@ "license": "MIT",

"require": "./dist/react/index.cjs"
},
"./vue": {
"types": "./dist/vue/index.d.ts",
"import": "./dist/vue/index.js",
"require": "./dist/vue/index.cjs"
},
"./svelte": {
"types": "./dist/svelte/index.d.ts",
"import": "./dist/svelte/index.js",
"require": "./dist/svelte/index.cjs"
}

@@ -42,3 +52,7 @@ },

"README.md",
"LICENSE"
"LICENSE",
"vue.js",
"vue.d.ts",
"svelte.js",
"svelte.d.ts"
],

@@ -55,2 +69,5 @@ "engines": {

"optional": true
},
"react": {
"optional": true
}

@@ -65,3 +82,3 @@ },

"vitest": "^2.1.0",
"@sonenta/react-i18next": "2.1.0"
"@sonenta/react-i18next": "2.4.0"
},

@@ -68,0 +85,0 @@ "publishConfig": {