@xenide-io/suite-client 0.5.0 → 0.6.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.
package/dist/index.js CHANGED
@@ -26,6 +26,8 @@ __export(index_exports, {
26
26
  ERROR_RETRYABLE: () => ERROR_RETRYABLE,
27
27
  FREE_PLAN_SEAT_HELP: () => FREE_PLAN_SEAT_HELP,
28
28
  ORGANISATION_SEAT_HELP: () => ORGANISATION_SEAT_HELP,
29
+ SUITE_APP_QUERY_CACHES: () => SUITE_APP_QUERY_CACHES,
30
+ SUITE_QUERY_CACHE_MAX_AGE_MS: () => SUITE_QUERY_CACHE_MAX_AGE_MS,
29
31
  TIDES_ACCESS_HELP: () => TIDES_ACCESS_HELP,
30
32
  TURTLETIME_ACCESS_HELP: () => TURTLETIME_ACCESS_HELP,
31
33
  WEBHOOK_ACTIVITY_ACTIONS: () => WEBHOOK_ACTIVITY_ACTIONS,
@@ -53,9 +55,12 @@ __export(index_exports, {
53
55
  formatAppError: () => formatAppError,
54
56
  hasAppAccess: () => hasAppAccess,
55
57
  isErrorCode: () => isErrorCode,
58
+ nextRevisionTick: () => nextRevisionTick,
56
59
  normalizeError: () => normalizeError,
57
60
  normalizeThrown: () => normalizeThrown,
61
+ persistSuiteQueries: () => persistSuiteQueries,
58
62
  planSeatSummary: () => planSeatSummary,
63
+ readJsonSnapshot: () => readJsonSnapshot,
59
64
  resolveSubscriptionTier: () => resolveSubscriptionTier,
60
65
  seatTypeDescription: () => seatTypeDescription,
61
66
  seatTypeFromFlags: () => seatTypeFromFlags,
@@ -63,11 +68,15 @@ __export(index_exports, {
63
68
  selectedToWebhookActions: () => selectedToWebhookActions,
64
69
  sortedSuiteAiEvents: () => sortedSuiteAiEvents,
65
70
  suiteAiEventSummary: () => suiteAiEventSummary,
71
+ suiteCacheBuster: () => suiteCacheBuster,
66
72
  tierAllowsTidesOnlySeats: () => tierAllowsTidesOnlySeats,
67
73
  tierAllowsTurtletime: () => tierAllowsTurtletime,
68
74
  tierLabel: () => tierLabel,
75
+ useEmailNotificationPreference: () => useEmailNotificationPreference,
69
76
  useIsNight: () => useIsNight,
70
- webhookActionsToSelected: () => webhookActionsToSelected
77
+ useSuiteRevisions: () => useSuiteRevisions,
78
+ webhookActionsToSelected: () => webhookActionsToSelected,
79
+ writeJsonSnapshot: () => writeJsonSnapshot
71
80
  });
72
81
  module.exports = __toCommonJS(index_exports);
73
82
 
@@ -9490,8 +9499,12 @@ function createAnalyticsClient(config) {
9490
9499
  defaults,
9491
9500
  person_profiles: "identified_only",
9492
9501
  autocapture: true,
9493
- capture_pageview: true,
9494
- capture_pageleave: true,
9502
+ // Pageviews are captured explicitly via `pageview()` (mount + route
9503
+ // change) so every `$pageview` carries the app/super-properties and the
9504
+ // behaviour is identical across all apps. The SDK's own pageview capture
9505
+ // is disabled to avoid double-counting and `defaults`-dependent behaviour.
9506
+ capture_pageview: false,
9507
+ capture_pageleave: false,
9495
9508
  persistence: "localStorage+cookie",
9496
9509
  cross_subdomain_cookie: true
9497
9510
  });
@@ -9549,6 +9562,7 @@ function createAnalyticsClient(config) {
9549
9562
  if (sessionId) headers["X-POSTHOG-SESSION-ID"] = sessionId;
9550
9563
  return headers;
9551
9564
  }
9565
+ if (isBrowser()) init();
9552
9566
  return {
9553
9567
  init,
9554
9568
  identify,
@@ -9575,6 +9589,8 @@ function createApiClient(config) {
9575
9589
  let accessToken = null;
9576
9590
  let refreshToken = null;
9577
9591
  let activeWorkspaceId = null;
9592
+ const etagBodies = /* @__PURE__ */ new Map();
9593
+ const pendingGets = /* @__PURE__ */ new Map();
9578
9594
  let authFailureHandler = null;
9579
9595
  let authFailureFired = false;
9580
9596
  let refreshPromise = null;
@@ -9666,6 +9682,7 @@ function createApiClient(config) {
9666
9682
  window.localStorage.removeItem(WORKSPACE_KEY);
9667
9683
  }
9668
9684
  if (changed) {
9685
+ etagBodies.clear();
9669
9686
  config.onWorkspaceChange?.(id2);
9670
9687
  if (id2 && previousWorkspaceId) {
9671
9688
  track("Workspace Switched", {
@@ -9699,6 +9716,8 @@ function createApiClient(config) {
9699
9716
  accessToken = null;
9700
9717
  refreshToken = null;
9701
9718
  activeWorkspaceId = null;
9719
+ etagBodies.clear();
9720
+ pendingGets.clear();
9702
9721
  if (typeof window !== "undefined") {
9703
9722
  window.localStorage.removeItem(ACCESS_KEY);
9704
9723
  window.localStorage.removeItem(REFRESH_KEY);
@@ -9767,6 +9786,30 @@ function createApiClient(config) {
9767
9786
  }
9768
9787
  }
9769
9788
  async function apiFetch(path, init = {}) {
9789
+ if ((init.method ?? "GET").toUpperCase() !== "GET") {
9790
+ pendingGets.clear();
9791
+ try {
9792
+ return await apiFetchInternal(path, init);
9793
+ } finally {
9794
+ pendingGets.clear();
9795
+ }
9796
+ }
9797
+ if (Object.keys(init).length > 0) return apiFetchInternal(path, init);
9798
+ const key = JSON.stringify([
9799
+ getAccessToken(),
9800
+ getActiveWorkspaceId(),
9801
+ path
9802
+ ]);
9803
+ let pending = pendingGets.get(key);
9804
+ if (!pending) {
9805
+ pending = apiFetchInternal(path, init).finally(() => {
9806
+ if (pendingGets.get(key) === pending) pendingGets.delete(key);
9807
+ });
9808
+ pendingGets.set(key, pending);
9809
+ }
9810
+ return (await pending).clone();
9811
+ }
9812
+ async function apiFetchInternal(path, init = {}) {
9770
9813
  const { skipAuth, ...requestInit } = init;
9771
9814
  const token = getAccessToken();
9772
9815
  const headers = {
@@ -9786,6 +9829,12 @@ function createApiClient(config) {
9786
9829
  headers["X-Active-Workspace-Id"] = wsId;
9787
9830
  }
9788
9831
  applyContextHeaders(headers);
9832
+ const method = (requestInit.method ?? "GET").toUpperCase();
9833
+ const etagKey = `${method}:${wsId ?? ""}:${path}`;
9834
+ if (method === "GET") {
9835
+ const hit = etagBodies.get(etagKey);
9836
+ if (hit) headers["If-None-Match"] = hit.etag;
9837
+ }
9789
9838
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
9790
9839
  if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
9791
9840
  throw new Error("Refusing to send the session to an unexpected origin.");
@@ -9817,6 +9866,26 @@ function createApiClient(config) {
9817
9866
  triggerAuthFailure();
9818
9867
  }
9819
9868
  }
9869
+ if (method === "GET" && res.status === 304) {
9870
+ const hit = etagBodies.get(etagKey);
9871
+ if (hit) {
9872
+ return new Response(hit.body, {
9873
+ status: 200,
9874
+ headers: { "Content-Type": "application/json", ETag: hit.etag }
9875
+ });
9876
+ }
9877
+ }
9878
+ if (method === "GET" && res.ok) {
9879
+ const etag = res.headers.get("ETag");
9880
+ if (etag) {
9881
+ const cachedBody = await res.clone().text();
9882
+ if (etagBodies.size >= 40) {
9883
+ const oldest = etagBodies.keys().next().value;
9884
+ if (oldest) etagBodies.delete(oldest);
9885
+ }
9886
+ etagBodies.set(etagKey, { etag, body: cachedBody });
9887
+ }
9888
+ }
9820
9889
  return res;
9821
9890
  }
9822
9891
  async function parseApiError(res) {
@@ -9911,8 +9980,6 @@ function createAuthReturnPath(config) {
9911
9980
  }
9912
9981
  }
9913
9982
  function readPortalHandoff(fallbackNext = DEFAULT_NEXT) {
9914
- const cached = cachedHandoff(fallbackNext);
9915
- if (cached) return cached;
9916
9983
  const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
9917
9984
  const query = new URLSearchParams(window.location.search);
9918
9985
  const nextRaw = hash.get("next") || query.get("next");
@@ -9925,19 +9992,18 @@ function createAuthReturnPath(config) {
9925
9992
  if (result.code || result.token) {
9926
9993
  sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));
9927
9994
  }
9928
- return result;
9995
+ return result.code || result.token ? result : cachedHandoff(fallbackNext) ?? result;
9929
9996
  }
9930
9997
  let redeemInflight = null;
9998
+ let redeemKey = "";
9931
9999
  function track(event, properties) {
9932
10000
  try {
9933
10001
  config.onAnalyticsEvent?.(event, properties);
9934
10002
  } catch {
9935
10003
  }
9936
10004
  }
9937
- async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
9938
- redeemInflight ?? (redeemInflight = (async () => {
9939
- const { code, token, refresh, next } = readPortalHandoff(fallbackNext);
9940
- if (!code) return { code: null, token, refresh, next };
10005
+ async function requestSwitchCode(apiUrl, code) {
10006
+ try {
9941
10007
  const response = await fetch(`${apiUrl.replace(/\/$/, "")}${REDEEM_PATH}`, {
9942
10008
  method: "POST",
9943
10009
  headers: {
@@ -9946,21 +10012,57 @@ function createAuthReturnPath(config) {
9946
10012
  },
9947
10013
  body: JSON.stringify({ code })
9948
10014
  });
9949
- if (!response.ok) {
10015
+ if (!response.ok) return "dead";
10016
+ return await response.json();
10017
+ } catch {
10018
+ return "retry";
10019
+ }
10020
+ }
10021
+ async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
10022
+ const handoff = readPortalHandoff(fallbackNext);
10023
+ const key = JSON.stringify([apiUrl, handoff]);
10024
+ if (redeemInflight && redeemKey === key) {
10025
+ return redeemInflight.then((result) => {
10026
+ if (result.token) sessionStorage.removeItem(HANDOFF_KEY);
10027
+ return result;
10028
+ });
10029
+ }
10030
+ redeemKey = key;
10031
+ const { code, token, refresh, next } = handoff;
10032
+ redeemInflight = (async () => {
10033
+ if (!code) {
10034
+ sessionStorage.removeItem(HANDOFF_KEY);
10035
+ return { code: null, token, refresh, next };
10036
+ }
10037
+ let outcome = await requestSwitchCode(apiUrl, code);
10038
+ if (outcome === "retry") outcome = await requestSwitchCode(apiUrl, code);
10039
+ if (outcome === "retry") {
10040
+ track("Cross-App Switch", { success: false });
10041
+ return { code: null, token: null, refresh: null, next };
10042
+ }
10043
+ if (outcome === "dead") {
10044
+ sessionStorage.removeItem(HANDOFF_KEY);
10045
+ track("Cross-App Switch", { success: false });
10046
+ return { code: null, token: null, refresh: null, next };
10047
+ }
10048
+ const redeemedToken = outcome.token || outcome.access || null;
10049
+ if (!redeemedToken) {
10050
+ sessionStorage.removeItem(HANDOFF_KEY);
9950
10051
  track("Cross-App Switch", { success: false });
9951
10052
  return { code: null, token: null, refresh: null, next };
9952
10053
  }
9953
- const data = await response.json();
9954
10054
  sessionStorage.removeItem(HANDOFF_KEY);
9955
- const redeemedToken = data.token || data.access || null;
9956
- track("Cross-App Switch", { success: Boolean(redeemedToken) });
10055
+ track("Cross-App Switch", { success: true });
9957
10056
  return {
9958
10057
  code: null,
9959
10058
  token: redeemedToken,
9960
- refresh: data.refresh || null,
10059
+ refresh: outcome.refresh || null,
9961
10060
  next
9962
10061
  };
9963
- })());
10062
+ })().then((result) => {
10063
+ if (!result.token && redeemKey === key) redeemInflight = null;
10064
+ return result;
10065
+ });
9964
10066
  return redeemInflight;
9965
10067
  }
9966
10068
  function readAuthReturnPath() {
@@ -10272,6 +10374,165 @@ function createWorkspaceAppearanceApi(config) {
10272
10374
  };
10273
10375
  }
10274
10376
 
10377
+ // src/cache/persist-query-cache.ts
10378
+ var SUITE_QUERY_CACHE_MAX_AGE_MS = 30 * 60 * 1e3;
10379
+ var MAX_SNAPSHOT_CHARS = 15e5;
10380
+ function suiteCacheBuster(tokenKey, workspaceKey) {
10381
+ if (typeof window === "undefined") return "";
10382
+ let token = null;
10383
+ let workspace = null;
10384
+ try {
10385
+ token = window.localStorage.getItem(tokenKey);
10386
+ workspace = window.localStorage.getItem(workspaceKey) || readCookie("xenide_active_workspace_id");
10387
+ } catch {
10388
+ return "";
10389
+ }
10390
+ const subject = subjectFromAccessToken(token);
10391
+ if (!subject || !workspace) return "";
10392
+ return `${subject}:${workspace}`;
10393
+ }
10394
+ function readJsonSnapshot(key, buster, maxAgeMs) {
10395
+ if (typeof window === "undefined" || !buster) return null;
10396
+ try {
10397
+ const raw = window.localStorage.getItem(key);
10398
+ if (!raw) return null;
10399
+ const parsed = JSON.parse(raw);
10400
+ if (parsed.buster !== buster || typeof parsed.savedAt !== "number") return null;
10401
+ if (Date.now() - parsed.savedAt > maxAgeMs) return null;
10402
+ return parsed.value ?? null;
10403
+ } catch {
10404
+ return null;
10405
+ }
10406
+ }
10407
+ function writeJsonSnapshot(key, buster, value) {
10408
+ if (typeof window === "undefined" || !buster) return;
10409
+ try {
10410
+ const raw = JSON.stringify({ buster, savedAt: Date.now(), value });
10411
+ if (raw.length > MAX_SNAPSHOT_CHARS) return;
10412
+ window.localStorage.setItem(key, raw);
10413
+ } catch {
10414
+ }
10415
+ }
10416
+ function persistSuiteQueries(queryClient, options) {
10417
+ const maxAgeMs = options.maxAgeMs ?? SUITE_QUERY_CACHE_MAX_AGE_MS;
10418
+ restore(queryClient, options, maxAgeMs);
10419
+ let timer = 0;
10420
+ const unsubscribe = queryClient.getQueryCache().subscribe(() => {
10421
+ if (typeof window === "undefined") return;
10422
+ window.clearTimeout(timer);
10423
+ timer = window.setTimeout(() => save(queryClient, options), 400);
10424
+ });
10425
+ return () => {
10426
+ if (typeof window !== "undefined") window.clearTimeout(timer);
10427
+ unsubscribe();
10428
+ };
10429
+ }
10430
+ function restore(queryClient, options, maxAgeMs) {
10431
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10432
+ const stored = readJsonSnapshot(
10433
+ options.storageKey,
10434
+ buster,
10435
+ maxAgeMs
10436
+ );
10437
+ if (!Array.isArray(stored)) return;
10438
+ for (const entry of stored) {
10439
+ if (!entry || !Array.isArray(entry.queryKey) || !allowed(entry.queryKey, options.allow)) {
10440
+ continue;
10441
+ }
10442
+ queryClient.setQueryData(entry.queryKey, entry.data);
10443
+ void queryClient.invalidateQueries({
10444
+ queryKey: entry.queryKey,
10445
+ refetchType: "none"
10446
+ });
10447
+ }
10448
+ }
10449
+ function save(queryClient, options) {
10450
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10451
+ if (!buster) return;
10452
+ const queries = queryClient.getQueryCache().getAll().filter(
10453
+ (query) => query.state.status === "success" && query.state.data !== void 0 && allowed(query.queryKey, options.allow)
10454
+ ).map((query) => ({ queryKey: query.queryKey, data: query.state.data }));
10455
+ writeJsonSnapshot(options.storageKey, buster, queries);
10456
+ }
10457
+ function allowed(queryKey, allow) {
10458
+ return allow.some((rule) => {
10459
+ if (typeof rule === "string") return queryKey[0] === rule;
10460
+ return rule.every((part, index) => queryKey[index] === part);
10461
+ });
10462
+ }
10463
+ function readCookie(name) {
10464
+ if (typeof document === "undefined") return null;
10465
+ const prefix = `${name}=`;
10466
+ const entry = document.cookie.split("; ").find((item) => item.startsWith(prefix));
10467
+ if (!entry) return null;
10468
+ try {
10469
+ return decodeURIComponent(entry.slice(prefix.length)) || null;
10470
+ } catch {
10471
+ return entry.slice(prefix.length) || null;
10472
+ }
10473
+ }
10474
+ function subjectFromAccessToken(token) {
10475
+ if (!token) return "";
10476
+ const segment = token.split(".")[1];
10477
+ if (!segment) return "";
10478
+ try {
10479
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
10480
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
10481
+ const json = JSON.parse(atob(padded));
10482
+ return typeof json.sub === "string" ? json.sub : "";
10483
+ } catch {
10484
+ return "";
10485
+ }
10486
+ }
10487
+ var SUITE_APP_QUERY_CACHES = {
10488
+ kraken: {
10489
+ storageKey: "kraken-query-cache:v1",
10490
+ tokenKey: "kraken_access_token",
10491
+ workspaceKey: "kraken_active_workspace_id",
10492
+ allow: ["kraken-sidebar"]
10493
+ },
10494
+ shellstack: {
10495
+ storageKey: "shellstack-query-cache:v1",
10496
+ tokenKey: "portal_access_token",
10497
+ workspaceKey: "portal_active_workspace_id",
10498
+ allow: ["calendar-events", "org-workspaces", "organisation", "org-team", "directory"]
10499
+ },
10500
+ tides: {
10501
+ storageKey: "tides-query-cache:v1",
10502
+ tokenKey: "tides_access_token",
10503
+ workspaceKey: "tides_active_workspace_id",
10504
+ allow: [
10505
+ ["tides", "projects"],
10506
+ ["tides", "clients"],
10507
+ ["tides", "workspace-members"],
10508
+ ["tides", "workspace-tags"],
10509
+ ["tides", "project-statuses"]
10510
+ ]
10511
+ },
10512
+ turtletime: {
10513
+ storageKey: "tt-query-cache:v1",
10514
+ tokenKey: "tt_access_token",
10515
+ workspaceKey: "tt_active_workspace_id",
10516
+ allow: [
10517
+ ["tt", "tracker-catalog"],
10518
+ ["tt", "projects"],
10519
+ ["tt", "clients"],
10520
+ ["tt", "categories"]
10521
+ ]
10522
+ },
10523
+ shelly: {
10524
+ storageKey: "shelly-query-cache:v1",
10525
+ tokenKey: "shelly_access_token",
10526
+ workspaceKey: "shelly_active_workspace_id",
10527
+ allow: [
10528
+ ["shelly", "skills"],
10529
+ ["shelly", "crews"],
10530
+ ["shelly", "mcp-servers"],
10531
+ ["shelly", "models"]
10532
+ ]
10533
+ }
10534
+ };
10535
+
10275
10536
  // src/sync/create-cross-app-sync.ts
10276
10537
  function createCrossAppSync(config) {
10277
10538
  const pollMs = config.pollMs ?? 5e3;
@@ -10283,40 +10544,49 @@ function createCrossAppSync(config) {
10283
10544
  const subscribers = [];
10284
10545
  let pollInterval = null;
10285
10546
  let pollListeners = 0;
10547
+ let inFlight = false;
10548
+ let primed = false;
10549
+ let scope;
10550
+ let lastRequestedScope;
10551
+ let lastRequestedAt;
10286
10552
  async function fetchCheckpoint() {
10553
+ if (inFlight || document.visibilityState !== "visible") return null;
10554
+ const requestScope = config.getScope?.();
10555
+ if (requestScope === lastRequestedScope && lastRequestedAt !== void 0 && Date.now() - lastRequestedAt < pollMs)
10556
+ return null;
10557
+ inFlight = true;
10558
+ lastRequestedScope = requestScope;
10559
+ lastRequestedAt = Date.now();
10287
10560
  try {
10288
10561
  const res = await config.api.get(config.checkpointPath);
10289
10562
  if (!res.ok) return null;
10290
- return await res.json();
10563
+ const data = await res.json();
10564
+ if (requestScope !== config.getScope?.()) return null;
10565
+ if (scope !== requestScope) {
10566
+ primed = false;
10567
+ scope = requestScope;
10568
+ }
10569
+ return data;
10291
10570
  } catch {
10292
10571
  return null;
10293
- }
10294
- }
10295
- function notify(topic) {
10296
- for (const sub of subscribers) {
10297
- if (!sub.topic || sub.topic === topic) {
10298
- if (lastKnown[topic] > sub.lastSync) {
10299
- sub.lastSync = lastKnown[topic];
10300
- sub.callback();
10301
- }
10302
- }
10572
+ } finally {
10573
+ inFlight = false;
10303
10574
  }
10304
10575
  }
10305
10576
  function checkAndNotify(data) {
10306
- const tasks = data.tasks_synced_at || 0;
10307
- const time = data.time_synced_at || 0;
10308
- const integration = data.integration_synced_at || 0;
10309
- if (tasks > lastKnown.tasks) {
10310
- lastKnown.tasks = tasks;
10311
- notify("tasks");
10312
- }
10313
- if (time > lastKnown.time) {
10314
- lastKnown.time = time;
10315
- notify("time");
10316
- }
10317
- if (integration > lastKnown.integration) {
10318
- lastKnown.integration = integration;
10319
- notify("integration");
10577
+ const next = {
10578
+ tasks: data.tasks_synced_at || 0,
10579
+ time: data.time_synced_at || 0,
10580
+ integration: data.integration_synced_at || 0
10581
+ };
10582
+ const changed = ["tasks", "time", "integration"].filter(
10583
+ (topic) => primed && next[topic] > lastKnown[topic]
10584
+ );
10585
+ Object.assign(lastKnown, next);
10586
+ primed = true;
10587
+ for (const sub of subscribers) {
10588
+ if (changed.some((topic) => !sub.topic || sub.topic === topic))
10589
+ sub.callback();
10320
10590
  }
10321
10591
  }
10322
10592
  function startPolling() {
@@ -10343,8 +10613,7 @@ function createCrossAppSync(config) {
10343
10613
  };
10344
10614
  const sub = {
10345
10615
  topic,
10346
- callback: onSync,
10347
- lastSync: lastKnown[topic || "tasks"]
10616
+ callback: onSync
10348
10617
  };
10349
10618
  subscribers.push(sub);
10350
10619
  pollListeners += 1;
@@ -10415,6 +10684,80 @@ function createLiveRefresh(sync) {
10415
10684
  return { subscribeLiveRefresh, hasPendingCrossAppSync };
10416
10685
  }
10417
10686
 
10687
+ // src/sync/use-suite-revisions.ts
10688
+ var import_react = require("react");
10689
+ function nextRevisionTick(seen, ready, data) {
10690
+ if (!data) return { seen, ready, changed: [] };
10691
+ const next = { ...seen };
10692
+ if (!ready) {
10693
+ for (const [topic, value] of Object.entries(data)) {
10694
+ if (typeof value === "number") next[topic] = value;
10695
+ }
10696
+ return { seen: next, ready: true, changed: [] };
10697
+ }
10698
+ const changed = [];
10699
+ for (const [topic, value] of Object.entries(data)) {
10700
+ if (typeof value !== "number" || next[topic] === value) continue;
10701
+ next[topic] = value;
10702
+ changed.push(topic);
10703
+ }
10704
+ return { seen: next, ready: true, changed };
10705
+ }
10706
+ function useSuiteRevisions(options) {
10707
+ const loadRef = (0, import_react.useRef)(options.load);
10708
+ const onTopicRef = (0, import_react.useRef)(options.onTopic);
10709
+ loadRef.current = options.load;
10710
+ onTopicRef.current = options.onTopic;
10711
+ const pollMs = options.pollMs ?? 15e3;
10712
+ const scope = options.scope ?? "";
10713
+ const seenScope = (0, import_react.useRef)(null);
10714
+ (0, import_react.useEffect)(() => {
10715
+ if (!options.enabled || typeof window === "undefined") return;
10716
+ const switching = seenScope.current !== null && seenScope.current !== scope;
10717
+ seenScope.current = scope;
10718
+ let seen = {};
10719
+ let ready = switching;
10720
+ let timer;
10721
+ let inFlight = false;
10722
+ let disposed = false;
10723
+ const tick = async () => {
10724
+ if (inFlight || disposed) return;
10725
+ inFlight = true;
10726
+ try {
10727
+ const data = await loadRef.current();
10728
+ if (disposed) return;
10729
+ const step = nextRevisionTick(seen, ready, data);
10730
+ seen = step.seen;
10731
+ ready = step.ready;
10732
+ for (const topic of step.changed) onTopicRef.current(topic);
10733
+ } catch {
10734
+ } finally {
10735
+ inFlight = false;
10736
+ }
10737
+ };
10738
+ const stop = () => {
10739
+ if (timer !== void 0) window.clearInterval(timer);
10740
+ timer = void 0;
10741
+ };
10742
+ const start = () => {
10743
+ if (timer !== void 0) return;
10744
+ void tick();
10745
+ timer = window.setInterval(() => void tick(), pollMs);
10746
+ };
10747
+ const onVisibility = () => {
10748
+ if (document.visibilityState === "visible") start();
10749
+ else stop();
10750
+ };
10751
+ if (document.visibilityState === "visible") start();
10752
+ document.addEventListener("visibilitychange", onVisibility);
10753
+ return () => {
10754
+ disposed = true;
10755
+ stop();
10756
+ document.removeEventListener("visibilitychange", onVisibility);
10757
+ };
10758
+ }, [options.enabled, pollMs, scope]);
10759
+ }
10760
+
10418
10761
  // src/notifications/create-notifications-client.ts
10419
10762
  function createNotificationsClient(config) {
10420
10763
  const { api, parseApiError } = config;
@@ -10560,6 +10903,47 @@ function createWebPushClient(config) {
10560
10903
  return { supported, getState, enable, disable };
10561
10904
  }
10562
10905
 
10906
+ // src/notifications/use-email-notification-preference.ts
10907
+ var import_react2 = require("react");
10908
+ var EMAIL_PREFERENCE_PATH = "/api/notifications/email/";
10909
+ function useEmailNotificationPreference(api, parseApiError) {
10910
+ const [enabled, setEnabled] = (0, import_react2.useState)(false);
10911
+ const [busy, setBusy] = (0, import_react2.useState)(false);
10912
+ const [error, setError] = (0, import_react2.useState)("");
10913
+ (0, import_react2.useEffect)(() => {
10914
+ let cancelled = false;
10915
+ api.get(EMAIL_PREFERENCE_PATH).then(async (res) => {
10916
+ if (!res.ok || cancelled) return;
10917
+ const data = await res.json();
10918
+ if (!cancelled) setEnabled(Boolean(data.enabled));
10919
+ }).catch(() => {
10920
+ });
10921
+ return () => {
10922
+ cancelled = true;
10923
+ };
10924
+ }, [api]);
10925
+ const onChange = (0, import_react2.useCallback)(
10926
+ (next) => {
10927
+ setBusy(true);
10928
+ setError("");
10929
+ setEnabled(next);
10930
+ api.patch(EMAIL_PREFERENCE_PATH, { enabled: next }).then(async (res) => {
10931
+ if (!res.ok) {
10932
+ setEnabled(!next);
10933
+ setError(await parseApiError(res));
10934
+ }
10935
+ }).catch((err) => {
10936
+ setEnabled(!next);
10937
+ setError(
10938
+ err instanceof Error ? err.message : "Could not save email notifications."
10939
+ );
10940
+ }).finally(() => setBusy(false));
10941
+ },
10942
+ [api, parseApiError]
10943
+ );
10944
+ return { enabled, busy, error, onChange };
10945
+ }
10946
+
10563
10947
  // src/ai/suite-ai-job.ts
10564
10948
  function activeSuiteAiJobs(jobs) {
10565
10949
  return jobs.filter(
@@ -10641,12 +11025,12 @@ function createJsonLd(config) {
10641
11025
  }
10642
11026
 
10643
11027
  // src/hooks/use-is-night.ts
10644
- var import_react = require("react");
11028
+ var import_react3 = require("react");
10645
11029
  var NIGHT_START_HOUR = 18;
10646
11030
  var NIGHT_END_HOUR = 6;
10647
11031
  function useIsNight() {
10648
- const [isNight, setIsNight] = (0, import_react.useState)(false);
10649
- (0, import_react.useEffect)(() => {
11032
+ const [isNight, setIsNight] = (0, import_react3.useState)(false);
11033
+ (0, import_react3.useEffect)(() => {
10650
11034
  const check = () => {
10651
11035
  const hour = (/* @__PURE__ */ new Date()).getHours();
10652
11036
  return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;
@@ -10671,6 +11055,8 @@ function useIsNight() {
10671
11055
  ERROR_RETRYABLE,
10672
11056
  FREE_PLAN_SEAT_HELP,
10673
11057
  ORGANISATION_SEAT_HELP,
11058
+ SUITE_APP_QUERY_CACHES,
11059
+ SUITE_QUERY_CACHE_MAX_AGE_MS,
10674
11060
  TIDES_ACCESS_HELP,
10675
11061
  TURTLETIME_ACCESS_HELP,
10676
11062
  WEBHOOK_ACTIVITY_ACTIONS,
@@ -10698,9 +11084,12 @@ function useIsNight() {
10698
11084
  formatAppError,
10699
11085
  hasAppAccess,
10700
11086
  isErrorCode,
11087
+ nextRevisionTick,
10701
11088
  normalizeError,
10702
11089
  normalizeThrown,
11090
+ persistSuiteQueries,
10703
11091
  planSeatSummary,
11092
+ readJsonSnapshot,
10704
11093
  resolveSubscriptionTier,
10705
11094
  seatTypeDescription,
10706
11095
  seatTypeFromFlags,
@@ -10708,10 +11097,14 @@ function useIsNight() {
10708
11097
  selectedToWebhookActions,
10709
11098
  sortedSuiteAiEvents,
10710
11099
  suiteAiEventSummary,
11100
+ suiteCacheBuster,
10711
11101
  tierAllowsTidesOnlySeats,
10712
11102
  tierAllowsTurtletime,
10713
11103
  tierLabel,
11104
+ useEmailNotificationPreference,
10714
11105
  useIsNight,
10715
- webhookActionsToSelected
11106
+ useSuiteRevisions,
11107
+ webhookActionsToSelected,
11108
+ writeJsonSnapshot
10716
11109
  });
10717
11110
  //# sourceMappingURL=index.js.map