@corbado/observe 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,139 @@
1
1
  // src/config-manager.ts
2
- import { cacheConfig, loadCachedConfig, parseReliabilityConfig } from "./config.mjs";
2
+ import {
3
+ cacheConfigRecord,
4
+ CONFIG_STORAGE_KEY,
5
+ loadCachedConfig,
6
+ loadConfigRecord,
7
+ parseReliabilityConfig
8
+ } from "./config.mjs";
3
9
  import { sendFetch } from "./readers/network.mjs";
10
+ import { listenWindow, readDocumentHidden } from "./readers/page.mjs";
4
11
  var CONFIG_FETCH_TIMEOUT_MS = 10 * 1e3;
5
- var CONFIG_REFRESH_INTERVAL_MS = 10 * 60 * 1e3;
12
+ var CONFIG_MIN_ATTEMPT_GAP_MS = 60 * 1e3;
13
+ var CONFIG_DEFAULT_TTL_MS = 60 * 60 * 1e3;
14
+ var CONFIG_MIN_TTL_MS = 5 * 60 * 1e3;
15
+ var CONFIG_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
6
16
  var CONFIG_RETRY_DELAYS_MS = [1e3, 3e3];
17
+ function configTtlMs(headers) {
18
+ const maxAge = /(?:^|[\s,])max-age\s*=\s*"?(\d+)"?/i.exec(headers?.get("Cache-Control") ?? "");
19
+ if (!maxAge) return CONFIG_DEFAULT_TTL_MS;
20
+ const age = Number(headers?.get("Age") ?? 0);
21
+ const ttlMs = (Number(maxAge[1]) - (Number.isFinite(age) && age > 0 ? age : 0)) * 1e3;
22
+ return Math.min(Math.max(ttlMs, CONFIG_MIN_TTL_MS), CONFIG_MAX_TTL_MS);
23
+ }
7
24
  var ConfigManager = class {
8
25
  constructor(url, storage, logger) {
9
26
  this.url = url;
10
27
  this.storage = storage;
11
28
  this.logger = logger;
12
29
  this.initialConfig = loadCachedConfig(storage) ?? void 0;
30
+ this.appliedVersion = this.initialConfig?.version;
13
31
  }
14
32
  initialConfig;
15
- refreshTimer;
16
33
  requestTimer;
17
34
  retryTimer;
18
35
  controller;
36
+ removers = [];
19
37
  fetching = false;
20
38
  stopped = false;
39
+ lastAttemptAt;
40
+ /** Version last handed to (or initially applied by) the tracker. */
41
+ appliedVersion;
21
42
  onConfig;
22
43
  start(onConfig) {
23
44
  if (this.stopped || this.onConfig) return;
24
45
  this.onConfig = onConfig;
25
- void this.fetchConfig();
46
+ this.listen();
47
+ this.refresh();
26
48
  }
27
- async fetchConfig(attempt = 0) {
28
- if (this.stopped || this.fetching) return;
29
- if (attempt === 0) {
30
- clearTimeout(this.refreshTimer);
49
+ listen() {
50
+ if (typeof window === "undefined") return;
51
+ const onVisibilityChange = () => {
52
+ const hidden = typeof document === "undefined" || readDocumentHidden({
53
+ key: "page:lifecycle.visibility",
54
+ title: "Page visibility",
55
+ accessNote: "document.visibilityState",
56
+ group: "Page lifecycle",
57
+ origin: "observe-external",
58
+ fields: [
59
+ {
60
+ path: "visibilityState",
61
+ label: "Whether the page is hidden",
62
+ exposure: "enum",
63
+ transform: { values: ["hidden", "other"] }
64
+ }
65
+ ]
66
+ });
67
+ if (!hidden) this.refresh();
68
+ };
69
+ this.removers.push(
70
+ listenWindow(
71
+ {
72
+ key: "page:lifecycle.visibilitychange",
73
+ title: "Page visibility changes",
74
+ accessNote: "visibilitychange event on document",
75
+ group: "Page lifecycle",
76
+ origin: "observe-external",
77
+ fields: [
78
+ {
79
+ path: "event.visibilitychange",
80
+ label: "Page hidden or shown again",
81
+ exposure: "presence",
82
+ transform: { note: "checks whether Observe settings are due for a refresh when the page is shown" }
83
+ }
84
+ ]
85
+ },
86
+ "visibilitychange",
87
+ onVisibilityChange
88
+ ),
89
+ listenWindow(
90
+ {
91
+ key: "page:lifecycle.pageshow",
92
+ title: "Returning to the page",
93
+ accessNote: "pageshow event on window, its persisted flag",
94
+ group: "Page lifecycle",
95
+ origin: "observe-external",
96
+ fields: [
97
+ {
98
+ path: "event.persisted",
99
+ label: "Page restored from the browser's back/forward cache",
100
+ exposure: "presence",
101
+ transform: { note: "checks whether Observe settings are due for a refresh" }
102
+ }
103
+ ]
104
+ },
105
+ "pageshow",
106
+ (event) => {
107
+ if (event.persisted) this.refresh();
108
+ }
109
+ )
110
+ );
111
+ }
112
+ /** Fetches when the cached config is stale; adopts a fresh config another tab stored meanwhile. */
113
+ refresh() {
114
+ try {
115
+ if (this.stopped || this.fetching) return;
116
+ const now = Date.now();
117
+ const record = loadConfigRecord(this.storage);
118
+ const { fetchedAt, expiresAt } = record ?? {};
119
+ if (record?.endpoint === this.url && typeof fetchedAt === "number" && typeof expiresAt === "number" && fetchedAt > 0 && expiresAt - fetchedAt <= CONFIG_MAX_TTL_MS && fetchedAt <= now && now < expiresAt) {
120
+ if (record.config.version !== this.appliedVersion) {
121
+ this.appliedVersion = record.config.version;
122
+ this.onConfig?.(record.config);
123
+ }
124
+ return;
125
+ }
126
+ const sinceAttempt = this.lastAttemptAt === void 0 ? Infinity : now - this.lastAttemptAt;
127
+ if (sinceAttempt >= 0 && sinceAttempt < CONFIG_MIN_ATTEMPT_GAP_MS) return;
128
+ this.lastAttemptAt = now;
31
129
  clearTimeout(this.retryTimer);
32
- this.refreshTimer = setTimeout(() => void this.fetchConfig(), CONFIG_REFRESH_INTERVAL_MS);
130
+ void this.fetchConfig();
131
+ } catch (error) {
132
+ this.logger.error("Unexpected error in SDK config refresh", error);
33
133
  }
134
+ }
135
+ async fetchConfig(attempt = 0) {
136
+ if (this.stopped || this.fetching) return;
34
137
  this.fetching = true;
35
138
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
36
139
  this.controller = controller;
@@ -51,6 +154,18 @@ var ConfigManager = class {
51
154
  label: "Whether the server asked to retry later",
52
155
  exposure: "presence"
53
156
  },
157
+ {
158
+ path: "response.header.cache-control",
159
+ label: "How long the server allows reusing the settings",
160
+ exposure: "computed",
161
+ transform: { note: "max-age, reduced to the settings' expiry time" }
162
+ },
163
+ {
164
+ path: "response.header.age",
165
+ label: "How long the settings were already stored on the way",
166
+ exposure: "computed",
167
+ transform: { note: "subtracted from max-age for the settings' expiry time" }
168
+ },
54
169
  {
55
170
  path: "response.body",
56
171
  label: "Observe settings",
@@ -62,7 +177,9 @@ var ConfigManager = class {
62
177
  this.url,
63
178
  {
64
179
  method: "GET",
65
- cache: "no-store",
180
+ // Revalidates the browser's HTTP cache entry (If-None-Match once the server sends an ETag)
181
+ // without a custom header, which would add a CORS preflight.
182
+ cache: "no-cache",
66
183
  ...controller ? { signal: controller.signal } : {}
67
184
  }
68
185
  );
@@ -75,7 +192,15 @@ var ConfigManager = class {
75
192
  const config = parseReliabilityConfig(raw);
76
193
  if (!config) throw new Error("Invalid SDK config response");
77
194
  if (this.stopped || controller?.signal.aborted) return;
78
- cacheConfig(this.storage, config);
195
+ const now = Date.now();
196
+ cacheConfigRecord(this.storage, {
197
+ endpoint: this.url,
198
+ fetchedAt: now,
199
+ expiresAt: now + configTtlMs(response.headers),
200
+ config
201
+ });
202
+ if (loadConfigRecord(this.storage)) this.storage.removeItem(CONFIG_STORAGE_KEY);
203
+ this.appliedVersion = config.version;
79
204
  this.onConfig?.(config);
80
205
  } catch (error) {
81
206
  if (!this.stopped) {
@@ -94,7 +219,7 @@ var ConfigManager = class {
94
219
  }
95
220
  destroy() {
96
221
  this.stopped = true;
97
- clearTimeout(this.refreshTimer);
222
+ this.removers.splice(0).forEach((remove) => remove());
98
223
  clearTimeout(this.requestTimer);
99
224
  clearTimeout(this.retryTimer);
100
225
  this.controller?.abort();
@@ -102,7 +227,11 @@ var ConfigManager = class {
102
227
  }
103
228
  };
104
229
  export {
230
+ CONFIG_DEFAULT_TTL_MS,
105
231
  CONFIG_FETCH_TIMEOUT_MS,
106
- CONFIG_REFRESH_INTERVAL_MS,
107
- ConfigManager
232
+ CONFIG_MAX_TTL_MS,
233
+ CONFIG_MIN_ATTEMPT_GAP_MS,
234
+ CONFIG_MIN_TTL_MS,
235
+ ConfigManager,
236
+ configTtlMs
108
237
  };
@@ -1,5 +1,6 @@
1
1
  // src/config.ts
2
2
  var CONFIG_STORAGE_KEY = "cbo_sdk_config";
3
+ var CONFIG_CACHE_STORAGE_KEY = "cbo_sdk_config_cache";
3
4
  var CONFIG_REQUEST_HEADER = "X-Corbado-Observe-Config";
4
5
  var DEFAULT_FLUSH_INTERVAL_MS = 500;
5
6
  var DEFAULT_SEQ_LOCK_TIMEOUT_MS = 50;
@@ -193,14 +194,20 @@ function resolveFixedDataConfig(raw) {
193
194
  return void 0;
194
195
  }
195
196
  }
197
+ function loadConfigRecord(storage) {
198
+ const record = storage.getItem(CONFIG_CACHE_STORAGE_KEY);
199
+ const config = record && typeof record === "object" ? parseReliabilityConfig(record.config) : null;
200
+ return config ? { ...record, config } : null;
201
+ }
196
202
  function loadCachedConfig(storage) {
197
- return parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
203
+ return loadConfigRecord(storage)?.config ?? parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
198
204
  }
199
- function cacheConfig(storage, config) {
200
- storage.setItem(CONFIG_STORAGE_KEY, config);
205
+ function cacheConfigRecord(storage, record) {
206
+ storage.setItem(CONFIG_CACHE_STORAGE_KEY, record);
201
207
  }
202
208
  export {
203
209
  CONFIG_BOUNDS,
210
+ CONFIG_CACHE_STORAGE_KEY,
204
211
  CONFIG_REQUEST_HEADER,
205
212
  CONFIG_STORAGE_KEY,
206
213
  DEFAULT_DEVICE_INFO_COLLECTOR_TIMEOUT_MS,
@@ -208,8 +215,9 @@ export {
208
215
  DEFAULT_RELIABILITY_CONFIG,
209
216
  DEFAULT_SEQ_LOCK_TIMEOUT_MS,
210
217
  DEFAULT_SESSION_INACTIVITY_MS,
211
- cacheConfig,
218
+ cacheConfigRecord,
212
219
  loadCachedConfig,
220
+ loadConfigRecord,
213
221
  parseReliabilityConfig,
214
222
  resolveFixedDataConfig,
215
223
  resolveReliabilityConfig
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  var SDK_NAME = "@corbado/observe";
3
- var SDK_VERSION = true ? "1.9.0" : "0.0.0-dev";
3
+ var SDK_VERSION = true ? "1.9.1" : "0.0.0-dev";
4
4
  export {
5
5
  SDK_NAME,
6
6
  SDK_VERSION