@braze/web-sdk
Version:
Braze SDK for web sites and other JS platforms.
553 lines (552 loc) • 16.2 kB
JavaScript
import t from "../common/base-provider.js";
import r from "../managers/braze-instance.js";
import { STORAGE_KEYS as s } from "../managers/storage-manager.js";
import f from "../managers/subscription-manager.js";
import l from "../util/net.js";
import h from "../util/request-header-utils.js";
import { logger as b } from "../../shared-lib/index.js";
import {
REQUEST_BACKOFF_MAX_SLEEP_MS_DEFAULT as c,
REQUEST_BACKOFF_MIN_SLEEP_MS_DEFAULT as d,
REQUEST_BACKOFF_SCALE_FACTOR_DEFAULT as m,
} from "../common/constants.js";
import { randomInclusive as a } from "../util/math.js";
import {
CONNECTION_VALID_THRESHOLD_MS as ii,
MAX_RETRIES as ti,
buildSseUrl as si,
} from "./dust-connection-core.js";
import {
DustWorkerBridge as ei,
isSharedWorkerSupported as ni,
} from "./dust-worker-bridge.js";
export default class sr extends t {
constructor(i, t, s, e, n = !0) {
super(),
(this.B = i),
(this.j = t),
(this.h = s),
(this.B = i),
(this.j = t),
(this.h = s),
(this.mite = null),
(this.Wi = null),
(this.Gi = null),
(this.Hi = null),
(this.Oi = e || null),
(this.Ki = null),
(this.Vi = null),
(this.T = null),
(this._i = 0),
(this.Qi = ti),
(this.Yi = null),
(this.Zi = null),
(this.we = !0),
(this.Se = null),
(this.ke = null),
(this.ye = null),
(this.Re = null),
(this.De = !1),
(this.Me = new Map()),
(this.xe = null),
(this.We = null),
(this.Fe = null),
(this.Ue = this.Ne(n)),
(this.Be = !1),
(this.Te = 0),
(this.ze = null),
this.Ae();
}
Ne(i) {
return i
? ni()
? "sharedworker"
: (b.info(
"SharedWorker not supported, using direct EventSource (multi-tab will gracefully degrade)",
),
"direct")
: (b.info("Shared connection disabled, using direct EventSource"),
"direct");
}
Ge() {
this.Fe = new ei({
Pe: (i) => {
this.qe(i);
},
He: () => {
this.Je(), (this.ze = new Date().valueOf());
},
Le: () => {
this.ze = null;
},
Oe: (i) => {
b.error(`Real-time messaging SharedWorker error: ${i}`);
},
});
this.Fe.initialize() ||
(b.info(
"SharedWorker initialization failed, falling back to direct EventSource",
),
(this.Ue = "direct"),
(this.Fe = null));
}
Je() {
(this._i = 0),
(this.Se = null),
(this.we = !0),
(this.Be = !1),
(this.Te = 0);
}
Ie() {
var i;
return (
!(
!("sharedworker" === this.Ue
? Boolean(null === (i = this.Fe) || void 0 === i ? void 0 : i.Ke())
: Boolean(this.Vi)) || null === this.ze
) && new Date().valueOf() - this.ze >= ii
);
}
qe(i) {
const t = i.type;
if (!t)
return void b.warn(
`Received real-time message without type: ${JSON.stringify(i)}`,
);
const s = "sharedworker" === this.Ue ? "SharedWorker" : "Direct";
b.info(`Received real-time message of type '${t}' via ${s}`);
const e = this.Me.get(t);
if (e && e.Ve() > 0)
try {
e.A(i);
} catch (i) {
b.error(`Error invoking subscription for message type '${t}': ${i}`);
}
else b.info(`No subscribers for real-time message type '${t}'`);
}
_e() {
return "sharedworker" === this.Ue;
}
Qe() {
return this.Ue;
}
Ae() {
if (this.j) {
const i = this.j.St(s.It.Xe),
t = this.j.St(s.It.Ye),
e = this.j.St(s.It.Ze),
n = this.j.St(s.It.sn);
i && t
? ((this.mite = i),
(this.Wi = t),
(this.Gi = e),
(this.Hi = n),
b.info("Restored real-time messaging configuration from storage"))
: (i || t) &&
(b.warn(
"Incomplete real-time messaging configuration in storage, clearing",
),
this.en());
}
}
Hr() {
this.xe ||
this.We ||
((this.xe = this.on("ddr", (i) => {
var t, s, e;
const n = i.body;
if (!n) return;
const r = n.r_ms;
if ("number" != typeof r) return;
const o = n.e,
h = (null === (t = this.h) || void 0 === t ? void 0 : t.vt()) || d,
l = (null === (s = this.h) || void 0 === s ? void 0 : s.gt()) || m,
u = (null === (e = this.h) || void 0 === e ? void 0 : e.bt()) || c,
g = Math.min(u, a(h, h * l)),
f = Math.round(r + g),
p = o ? ` (${o})` : "";
b.info(
`Admin requested disconnect${p}, reconnecting in ${f}ms (r_ms=${r} + backoff=${g})`,
),
this.hn(),
setTimeout(() => this.an(), f);
})),
(this.We = this.on("ttl", (i) => {
const t = i.body;
if (!t) return;
const s = t.t_ms;
if ("number" != typeof s) return;
const e = s;
if (
(b.info(`Time to live set to ${e}ms, will reconnect when expired`),
"sharedworker" === this.Ue)
)
return;
const n = t.rcs;
"string" == typeof n && (this.Ki = n),
null !== this.Zi && window.clearTimeout(this.Zi),
(this.Zi = window.setTimeout(() => {
(this.Zi = null),
b.info("Time to live expired, performing gapless reconnection"),
this.ln();
}, e));
})));
}
cn() {
this.ke ||
this.ye ||
((this.Re = () => {
(this.De = !0), (this.we = !1);
}),
window.addEventListener("beforeunload", this.Re),
(this.ke = () => {
var i;
this.De = !0;
(this.Vi ||
(null === (i = this.Fe) || void 0 === i ? void 0 : i.Ke())) &&
(b.info("Page unloading, closing real-time connection gracefully"),
(this.we = !1),
this.hn());
}),
window.addEventListener("pagehide", this.ke),
(this.ye = (i) => {
var t;
const s =
this.Vi || (null === (t = this.Fe) || void 0 === t ? void 0 : t.Ke());
i.persisted &&
this.un() &&
!s &&
(b.info("Page restored from bfcache, reconnecting"),
(this.De = !1),
this.Je(),
this.an());
}),
window.addEventListener("pageshow", this.ye));
}
Et() {
return this.T;
}
Lt(i) {
this.T = i;
}
on(i, t) {
if ("function" != typeof t) return null;
let s = this.Me.get(i);
return s || ((s = new f()), this.Me.set(i, s), r.S(s)), s.Ut(t);
}
dn(i, t) {
const s = this.Me.get(i);
s && s.removeSubscription(t);
}
un() {
return Boolean(this.mite && this.Wi);
}
gn() {
if (!this.Hi) return !1;
return Math.floor(new Date().valueOf() / 1e3) >= this.Hi;
}
en() {
(this.mite = null),
(this.Wi = null),
(this.Gi = null),
(this.Hi = null),
(this.Ki = null),
this.j &&
(this.j.Vt(s.It.Xe),
this.j.Vt(s.It.Ye),
this.j.Vt(s.It.Ze),
this.j.Vt(s.It.sn));
}
mn(i, t) {
const e = () => {
"function" == typeof t && t();
},
n = this.B,
r = this.j;
if (!n || !r)
return (
b.error("NetworkManager or StorageManager not available"), void e()
);
if (!this.h || !this.h.fn())
return (
b.info("Real-time messaging is not enabled, skipping refresh"), void e()
);
this.un()
? b.info("Refreshing real-time messaging configuration")
: b.info("Fetching initial real-time messaging configuration");
const o = n.Z({}, !0),
a = n.tt(o, h.it.pn),
c = new Date().valueOf();
h.nt(r, h.it.pn, c),
l.ot({
url: `${n.ht()}/dust/config`,
headers: a,
data: o,
lt: (t) => {
if (!n.ut(o, t, a))
return (
b.error(
"Failed to validate server response for real-time messaging configuration",
),
void e()
);
n.ct(),
t.mite && t.host
? ((this.mite = t.mite),
(this.Wi = t.host),
(this.Gi = t.auth || null),
(this.Hi = t.expiration || null),
b.info(
"Received real-time messaging configuration from server",
),
r.Pt(s.It.Xe, t.mite),
r.Pt(s.It.Ye, t.host),
t.auth ? r.Pt(s.It.Ze, t.auth) : r.Vt(s.It.Ze),
t.expiration ? r.Pt(s.It.sn, t.expiration) : r.Vt(s.It.sn),
this.an(),
"function" == typeof i && i())
: (b.info(
"Real-time messaging configuration not available - this SDK version may not be supported",
),
this.en(),
e());
},
error: (i) => {
n.dt(i, "retrieving DUST config"), e();
},
});
}
an() {
if (!this.h || !this.h.fn()) return;
if (!this.un())
return void b.error(
"Cannot start real-time subscription without configuration",
);
if (this.gn())
return (
b.info(
"Real-time messaging auth token has expired, refreshing configuration",
),
void this.mn(
() => {
this.an();
},
() => {
b.error(
"Failed to refresh expired real-time messaging configuration",
);
},
)
);
const i = this.mite,
t = this.Oi || this.Wi;
if (i && t)
switch (this.Ue) {
case "sharedworker":
this.vn(i, t);
break;
case "direct":
this.Vi &&
(b.info(
"Real-time connection already exists, closing before starting new subscription",
),
this.hn()),
this.wn();
}
}
bn() {
const i = this.mite,
t = this.Oi || this.Wi;
return i && t
? si(t, i, this._i, this.Gi || void 0, this.Ki || void 0)
: null;
}
vn(i, t) {
var s, e, n;
if ((this.Fe || this.Ge(), !this.Fe))
return (
b.info(
"SharedWorker initialization failed, falling back to direct EventSource",
),
(this.Ue = "direct"),
void this.Sn()
);
this.Oi && b.info(`Using custom real-time messaging host: ${this.Oi}`),
b.info("Starting real-time subscription via SharedWorker");
const r = (null === (s = this.h) || void 0 === s ? void 0 : s.vt()) || d,
o = (null === (e = this.h) || void 0 === e ? void 0 : e.bt()) || c,
h = (null === (n = this.h) || void 0 === n ? void 0 : n.gt()) || m;
this.Fe.connect({
mite: i,
dustHost: t,
auth: this.Gi || void 0,
backoff: { minSleepMs: r, maxSleepMs: o, scaleFactor: h },
});
}
Sn() {
this.Vi &&
(b.info(
"Real-time connection already exists, closing before starting new subscription",
),
this.kn()),
this.wn();
}
wn(i) {
const t = this.bn();
if (t) {
this.Oi && b.info(`Using custom real-time messaging host: ${this.Oi}`);
try {
const s = new EventSource(t);
(s.onopen = () => {
i
? (b.info(
"Gapless reconnection: new connection established, closing old connection",
),
i.close())
: b.info("Real-time messaging connection established"),
(this.Vi = s),
(this._i = 0),
(this.Se = null),
(this.we = !0),
(this.Be = !0),
(this.ze = new Date().valueOf());
}),
s.addEventListener("msg", (i) => {
this.yn(i.data);
}),
(s.onerror = () => {
const t = s.readyState;
return (
this.De ||
(0 === t
? b.info("Real-time messaging failed to connect")
: b.info("Real-time messaging connection lost")),
i && this.Vi !== s
? (b.info(
"Gapless reconnection failed, keeping old connection",
),
s.close(),
void (this.we && this._i < this.Qi && this.$n()))
: (this.kn(),
this.Be && (this.Te++, this.Te > 1)
? (b.info(
"Real-time messaging connection lost twice after successful connect (likely multi-tab conflict), yielding to other tab",
),
void (this.we = !1))
: void (this.we && this._i < this.Qi
? this.$n()
: (this._i >= this.Qi &&
b.error(
`Max retry attempts (${this.Qi}) reached for real-time messaging, giving up for current session`,
),
(this.we = !1))))
);
});
} catch (i) {
b.error(
`Failed to create real-time messaging connection: ${
i instanceof Error ? i.message : String(i)
}`,
);
}
}
}
$n() {
var i, t, s;
this._i++;
const e = (null === (i = this.h) || void 0 === i ? void 0 : i.vt()) || d,
n = (null === (t = this.h) || void 0 === t ? void 0 : t.gt()) || m,
r = (null === (s = this.h) || void 0 === s ? void 0 : s.bt()) || c;
let o = this.Se;
(null == o || o < e) && (o = e);
const h = Math.min(r, a(e, o * n));
(this.Se = h),
b.info(
`Retrying real-time messaging connection in ${h}ms (attempt ${this._i}/${this.Qi})`,
),
(this.Yi = window.setTimeout(() => {
(this.Yi = null), this.an();
}, h));
}
ln() {
if (this.un()) {
if (this.gn())
return (
b.info(
"Auth token expired during gapless reconnection, falling back to regular reconnection",
),
this.hn(),
void this.an()
);
null !== this.Zi && (window.clearTimeout(this.Zi), (this.Zi = null)),
this.wn(this.Vi || void 0);
} else b.error("Cannot perform gapless reconnection without configuration");
}
hn() {
var i;
switch (((this.ze = null), this.Ue)) {
case "sharedworker":
null === (i = this.Fe) || void 0 === i || i.disconnect();
break;
case "direct":
this.kn();
}
}
kn() {
null !== this.Yi && (window.clearTimeout(this.Yi), (this.Yi = null)),
null !== this.Zi && (window.clearTimeout(this.Zi), (this.Zi = null)),
(this.ze = null),
this.Vi &&
(this.Vi.close(),
(this.Vi = null),
b.info("Real-time messaging connection closed"));
}
yn(i) {
try {
const t = JSON.parse(i);
this.qe(t);
} catch (i) {
b.warn(
`Failed to parse real-time message: ${
i instanceof Error ? i.message : String(i)
}`,
);
}
}
changeUser(i = !1) {
this.hn(),
i ||
(this.un() &&
b.info(
"Clearing cached real-time messaging configuration for user change",
),
this.en()),
this.Je();
}
clearData(i = !1) {
(this.we = !1),
this.hn(),
i &&
(this.un() &&
b.info(
"Clearing cached real-time messaging configuration (wipeData)",
),
this.en()),
this.Je();
}
destroy() {
(this.we = !1),
this.hn(),
this.en(),
this.Fe && (this.Fe.destroy(), (this.Fe = null)),
this.xe && (this.dn("ddr", this.xe), (this.xe = null)),
this.We && (this.dn("ttl", this.We), (this.We = null)),
this.Re &&
(window.removeEventListener("beforeunload", this.Re), (this.Re = null)),
this.ke &&
(window.removeEventListener("pagehide", this.ke), (this.ke = null)),
this.ye &&
(window.removeEventListener("pageshow", this.ye), (this.ye = null)),
this.T && (r.removeSubscription(this.T), (this.T = null));
}
}