@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.mjs CHANGED
@@ -9417,8 +9417,12 @@ function createAnalyticsClient(config) {
9417
9417
  defaults,
9418
9418
  person_profiles: "identified_only",
9419
9419
  autocapture: true,
9420
- capture_pageview: true,
9421
- capture_pageleave: true,
9420
+ // Pageviews are captured explicitly via `pageview()` (mount + route
9421
+ // change) so every `$pageview` carries the app/super-properties and the
9422
+ // behaviour is identical across all apps. The SDK's own pageview capture
9423
+ // is disabled to avoid double-counting and `defaults`-dependent behaviour.
9424
+ capture_pageview: false,
9425
+ capture_pageleave: false,
9422
9426
  persistence: "localStorage+cookie",
9423
9427
  cross_subdomain_cookie: true
9424
9428
  });
@@ -9476,6 +9480,7 @@ function createAnalyticsClient(config) {
9476
9480
  if (sessionId) headers["X-POSTHOG-SESSION-ID"] = sessionId;
9477
9481
  return headers;
9478
9482
  }
9483
+ if (isBrowser()) init();
9479
9484
  return {
9480
9485
  init,
9481
9486
  identify,
@@ -9502,6 +9507,8 @@ function createApiClient(config) {
9502
9507
  let accessToken = null;
9503
9508
  let refreshToken = null;
9504
9509
  let activeWorkspaceId = null;
9510
+ const etagBodies = /* @__PURE__ */ new Map();
9511
+ const pendingGets = /* @__PURE__ */ new Map();
9505
9512
  let authFailureHandler = null;
9506
9513
  let authFailureFired = false;
9507
9514
  let refreshPromise = null;
@@ -9593,6 +9600,7 @@ function createApiClient(config) {
9593
9600
  window.localStorage.removeItem(WORKSPACE_KEY);
9594
9601
  }
9595
9602
  if (changed) {
9603
+ etagBodies.clear();
9596
9604
  config.onWorkspaceChange?.(id2);
9597
9605
  if (id2 && previousWorkspaceId) {
9598
9606
  track("Workspace Switched", {
@@ -9626,6 +9634,8 @@ function createApiClient(config) {
9626
9634
  accessToken = null;
9627
9635
  refreshToken = null;
9628
9636
  activeWorkspaceId = null;
9637
+ etagBodies.clear();
9638
+ pendingGets.clear();
9629
9639
  if (typeof window !== "undefined") {
9630
9640
  window.localStorage.removeItem(ACCESS_KEY);
9631
9641
  window.localStorage.removeItem(REFRESH_KEY);
@@ -9694,6 +9704,30 @@ function createApiClient(config) {
9694
9704
  }
9695
9705
  }
9696
9706
  async function apiFetch(path, init = {}) {
9707
+ if ((init.method ?? "GET").toUpperCase() !== "GET") {
9708
+ pendingGets.clear();
9709
+ try {
9710
+ return await apiFetchInternal(path, init);
9711
+ } finally {
9712
+ pendingGets.clear();
9713
+ }
9714
+ }
9715
+ if (Object.keys(init).length > 0) return apiFetchInternal(path, init);
9716
+ const key = JSON.stringify([
9717
+ getAccessToken(),
9718
+ getActiveWorkspaceId(),
9719
+ path
9720
+ ]);
9721
+ let pending = pendingGets.get(key);
9722
+ if (!pending) {
9723
+ pending = apiFetchInternal(path, init).finally(() => {
9724
+ if (pendingGets.get(key) === pending) pendingGets.delete(key);
9725
+ });
9726
+ pendingGets.set(key, pending);
9727
+ }
9728
+ return (await pending).clone();
9729
+ }
9730
+ async function apiFetchInternal(path, init = {}) {
9697
9731
  const { skipAuth, ...requestInit } = init;
9698
9732
  const token = getAccessToken();
9699
9733
  const headers = {
@@ -9713,6 +9747,12 @@ function createApiClient(config) {
9713
9747
  headers["X-Active-Workspace-Id"] = wsId;
9714
9748
  }
9715
9749
  applyContextHeaders(headers);
9750
+ const method = (requestInit.method ?? "GET").toUpperCase();
9751
+ const etagKey = `${method}:${wsId ?? ""}:${path}`;
9752
+ if (method === "GET") {
9753
+ const hit = etagBodies.get(etagKey);
9754
+ if (hit) headers["If-None-Match"] = hit.etag;
9755
+ }
9716
9756
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
9717
9757
  if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
9718
9758
  throw new Error("Refusing to send the session to an unexpected origin.");
@@ -9744,6 +9784,26 @@ function createApiClient(config) {
9744
9784
  triggerAuthFailure();
9745
9785
  }
9746
9786
  }
9787
+ if (method === "GET" && res.status === 304) {
9788
+ const hit = etagBodies.get(etagKey);
9789
+ if (hit) {
9790
+ return new Response(hit.body, {
9791
+ status: 200,
9792
+ headers: { "Content-Type": "application/json", ETag: hit.etag }
9793
+ });
9794
+ }
9795
+ }
9796
+ if (method === "GET" && res.ok) {
9797
+ const etag = res.headers.get("ETag");
9798
+ if (etag) {
9799
+ const cachedBody = await res.clone().text();
9800
+ if (etagBodies.size >= 40) {
9801
+ const oldest = etagBodies.keys().next().value;
9802
+ if (oldest) etagBodies.delete(oldest);
9803
+ }
9804
+ etagBodies.set(etagKey, { etag, body: cachedBody });
9805
+ }
9806
+ }
9747
9807
  return res;
9748
9808
  }
9749
9809
  async function parseApiError(res) {
@@ -9838,8 +9898,6 @@ function createAuthReturnPath(config) {
9838
9898
  }
9839
9899
  }
9840
9900
  function readPortalHandoff(fallbackNext = DEFAULT_NEXT) {
9841
- const cached = cachedHandoff(fallbackNext);
9842
- if (cached) return cached;
9843
9901
  const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
9844
9902
  const query = new URLSearchParams(window.location.search);
9845
9903
  const nextRaw = hash.get("next") || query.get("next");
@@ -9852,19 +9910,18 @@ function createAuthReturnPath(config) {
9852
9910
  if (result.code || result.token) {
9853
9911
  sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));
9854
9912
  }
9855
- return result;
9913
+ return result.code || result.token ? result : cachedHandoff(fallbackNext) ?? result;
9856
9914
  }
9857
9915
  let redeemInflight = null;
9916
+ let redeemKey = "";
9858
9917
  function track(event, properties) {
9859
9918
  try {
9860
9919
  config.onAnalyticsEvent?.(event, properties);
9861
9920
  } catch {
9862
9921
  }
9863
9922
  }
9864
- async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
9865
- redeemInflight ?? (redeemInflight = (async () => {
9866
- const { code, token, refresh, next } = readPortalHandoff(fallbackNext);
9867
- if (!code) return { code: null, token, refresh, next };
9923
+ async function requestSwitchCode(apiUrl, code) {
9924
+ try {
9868
9925
  const response = await fetch(`${apiUrl.replace(/\/$/, "")}${REDEEM_PATH}`, {
9869
9926
  method: "POST",
9870
9927
  headers: {
@@ -9873,21 +9930,57 @@ function createAuthReturnPath(config) {
9873
9930
  },
9874
9931
  body: JSON.stringify({ code })
9875
9932
  });
9876
- if (!response.ok) {
9933
+ if (!response.ok) return "dead";
9934
+ return await response.json();
9935
+ } catch {
9936
+ return "retry";
9937
+ }
9938
+ }
9939
+ async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
9940
+ const handoff = readPortalHandoff(fallbackNext);
9941
+ const key = JSON.stringify([apiUrl, handoff]);
9942
+ if (redeemInflight && redeemKey === key) {
9943
+ return redeemInflight.then((result) => {
9944
+ if (result.token) sessionStorage.removeItem(HANDOFF_KEY);
9945
+ return result;
9946
+ });
9947
+ }
9948
+ redeemKey = key;
9949
+ const { code, token, refresh, next } = handoff;
9950
+ redeemInflight = (async () => {
9951
+ if (!code) {
9952
+ sessionStorage.removeItem(HANDOFF_KEY);
9953
+ return { code: null, token, refresh, next };
9954
+ }
9955
+ let outcome = await requestSwitchCode(apiUrl, code);
9956
+ if (outcome === "retry") outcome = await requestSwitchCode(apiUrl, code);
9957
+ if (outcome === "retry") {
9958
+ track("Cross-App Switch", { success: false });
9959
+ return { code: null, token: null, refresh: null, next };
9960
+ }
9961
+ if (outcome === "dead") {
9962
+ sessionStorage.removeItem(HANDOFF_KEY);
9963
+ track("Cross-App Switch", { success: false });
9964
+ return { code: null, token: null, refresh: null, next };
9965
+ }
9966
+ const redeemedToken = outcome.token || outcome.access || null;
9967
+ if (!redeemedToken) {
9968
+ sessionStorage.removeItem(HANDOFF_KEY);
9877
9969
  track("Cross-App Switch", { success: false });
9878
9970
  return { code: null, token: null, refresh: null, next };
9879
9971
  }
9880
- const data = await response.json();
9881
9972
  sessionStorage.removeItem(HANDOFF_KEY);
9882
- const redeemedToken = data.token || data.access || null;
9883
- track("Cross-App Switch", { success: Boolean(redeemedToken) });
9973
+ track("Cross-App Switch", { success: true });
9884
9974
  return {
9885
9975
  code: null,
9886
9976
  token: redeemedToken,
9887
- refresh: data.refresh || null,
9977
+ refresh: outcome.refresh || null,
9888
9978
  next
9889
9979
  };
9890
- })());
9980
+ })().then((result) => {
9981
+ if (!result.token && redeemKey === key) redeemInflight = null;
9982
+ return result;
9983
+ });
9891
9984
  return redeemInflight;
9892
9985
  }
9893
9986
  function readAuthReturnPath() {
@@ -10199,6 +10292,165 @@ function createWorkspaceAppearanceApi(config) {
10199
10292
  };
10200
10293
  }
10201
10294
 
10295
+ // src/cache/persist-query-cache.ts
10296
+ var SUITE_QUERY_CACHE_MAX_AGE_MS = 30 * 60 * 1e3;
10297
+ var MAX_SNAPSHOT_CHARS = 15e5;
10298
+ function suiteCacheBuster(tokenKey, workspaceKey) {
10299
+ if (typeof window === "undefined") return "";
10300
+ let token = null;
10301
+ let workspace = null;
10302
+ try {
10303
+ token = window.localStorage.getItem(tokenKey);
10304
+ workspace = window.localStorage.getItem(workspaceKey) || readCookie("xenide_active_workspace_id");
10305
+ } catch {
10306
+ return "";
10307
+ }
10308
+ const subject = subjectFromAccessToken(token);
10309
+ if (!subject || !workspace) return "";
10310
+ return `${subject}:${workspace}`;
10311
+ }
10312
+ function readJsonSnapshot(key, buster, maxAgeMs) {
10313
+ if (typeof window === "undefined" || !buster) return null;
10314
+ try {
10315
+ const raw = window.localStorage.getItem(key);
10316
+ if (!raw) return null;
10317
+ const parsed = JSON.parse(raw);
10318
+ if (parsed.buster !== buster || typeof parsed.savedAt !== "number") return null;
10319
+ if (Date.now() - parsed.savedAt > maxAgeMs) return null;
10320
+ return parsed.value ?? null;
10321
+ } catch {
10322
+ return null;
10323
+ }
10324
+ }
10325
+ function writeJsonSnapshot(key, buster, value) {
10326
+ if (typeof window === "undefined" || !buster) return;
10327
+ try {
10328
+ const raw = JSON.stringify({ buster, savedAt: Date.now(), value });
10329
+ if (raw.length > MAX_SNAPSHOT_CHARS) return;
10330
+ window.localStorage.setItem(key, raw);
10331
+ } catch {
10332
+ }
10333
+ }
10334
+ function persistSuiteQueries(queryClient, options) {
10335
+ const maxAgeMs = options.maxAgeMs ?? SUITE_QUERY_CACHE_MAX_AGE_MS;
10336
+ restore(queryClient, options, maxAgeMs);
10337
+ let timer = 0;
10338
+ const unsubscribe = queryClient.getQueryCache().subscribe(() => {
10339
+ if (typeof window === "undefined") return;
10340
+ window.clearTimeout(timer);
10341
+ timer = window.setTimeout(() => save(queryClient, options), 400);
10342
+ });
10343
+ return () => {
10344
+ if (typeof window !== "undefined") window.clearTimeout(timer);
10345
+ unsubscribe();
10346
+ };
10347
+ }
10348
+ function restore(queryClient, options, maxAgeMs) {
10349
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10350
+ const stored = readJsonSnapshot(
10351
+ options.storageKey,
10352
+ buster,
10353
+ maxAgeMs
10354
+ );
10355
+ if (!Array.isArray(stored)) return;
10356
+ for (const entry of stored) {
10357
+ if (!entry || !Array.isArray(entry.queryKey) || !allowed(entry.queryKey, options.allow)) {
10358
+ continue;
10359
+ }
10360
+ queryClient.setQueryData(entry.queryKey, entry.data);
10361
+ void queryClient.invalidateQueries({
10362
+ queryKey: entry.queryKey,
10363
+ refetchType: "none"
10364
+ });
10365
+ }
10366
+ }
10367
+ function save(queryClient, options) {
10368
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10369
+ if (!buster) return;
10370
+ const queries = queryClient.getQueryCache().getAll().filter(
10371
+ (query) => query.state.status === "success" && query.state.data !== void 0 && allowed(query.queryKey, options.allow)
10372
+ ).map((query) => ({ queryKey: query.queryKey, data: query.state.data }));
10373
+ writeJsonSnapshot(options.storageKey, buster, queries);
10374
+ }
10375
+ function allowed(queryKey, allow) {
10376
+ return allow.some((rule) => {
10377
+ if (typeof rule === "string") return queryKey[0] === rule;
10378
+ return rule.every((part, index) => queryKey[index] === part);
10379
+ });
10380
+ }
10381
+ function readCookie(name) {
10382
+ if (typeof document === "undefined") return null;
10383
+ const prefix = `${name}=`;
10384
+ const entry = document.cookie.split("; ").find((item) => item.startsWith(prefix));
10385
+ if (!entry) return null;
10386
+ try {
10387
+ return decodeURIComponent(entry.slice(prefix.length)) || null;
10388
+ } catch {
10389
+ return entry.slice(prefix.length) || null;
10390
+ }
10391
+ }
10392
+ function subjectFromAccessToken(token) {
10393
+ if (!token) return "";
10394
+ const segment = token.split(".")[1];
10395
+ if (!segment) return "";
10396
+ try {
10397
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
10398
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
10399
+ const json = JSON.parse(atob(padded));
10400
+ return typeof json.sub === "string" ? json.sub : "";
10401
+ } catch {
10402
+ return "";
10403
+ }
10404
+ }
10405
+ var SUITE_APP_QUERY_CACHES = {
10406
+ kraken: {
10407
+ storageKey: "kraken-query-cache:v1",
10408
+ tokenKey: "kraken_access_token",
10409
+ workspaceKey: "kraken_active_workspace_id",
10410
+ allow: ["kraken-sidebar"]
10411
+ },
10412
+ shellstack: {
10413
+ storageKey: "shellstack-query-cache:v1",
10414
+ tokenKey: "portal_access_token",
10415
+ workspaceKey: "portal_active_workspace_id",
10416
+ allow: ["calendar-events", "org-workspaces", "organisation", "org-team", "directory"]
10417
+ },
10418
+ tides: {
10419
+ storageKey: "tides-query-cache:v1",
10420
+ tokenKey: "tides_access_token",
10421
+ workspaceKey: "tides_active_workspace_id",
10422
+ allow: [
10423
+ ["tides", "projects"],
10424
+ ["tides", "clients"],
10425
+ ["tides", "workspace-members"],
10426
+ ["tides", "workspace-tags"],
10427
+ ["tides", "project-statuses"]
10428
+ ]
10429
+ },
10430
+ turtletime: {
10431
+ storageKey: "tt-query-cache:v1",
10432
+ tokenKey: "tt_access_token",
10433
+ workspaceKey: "tt_active_workspace_id",
10434
+ allow: [
10435
+ ["tt", "tracker-catalog"],
10436
+ ["tt", "projects"],
10437
+ ["tt", "clients"],
10438
+ ["tt", "categories"]
10439
+ ]
10440
+ },
10441
+ shelly: {
10442
+ storageKey: "shelly-query-cache:v1",
10443
+ tokenKey: "shelly_access_token",
10444
+ workspaceKey: "shelly_active_workspace_id",
10445
+ allow: [
10446
+ ["shelly", "skills"],
10447
+ ["shelly", "crews"],
10448
+ ["shelly", "mcp-servers"],
10449
+ ["shelly", "models"]
10450
+ ]
10451
+ }
10452
+ };
10453
+
10202
10454
  // src/sync/create-cross-app-sync.ts
10203
10455
  function createCrossAppSync(config) {
10204
10456
  const pollMs = config.pollMs ?? 5e3;
@@ -10210,40 +10462,49 @@ function createCrossAppSync(config) {
10210
10462
  const subscribers = [];
10211
10463
  let pollInterval = null;
10212
10464
  let pollListeners = 0;
10465
+ let inFlight = false;
10466
+ let primed = false;
10467
+ let scope;
10468
+ let lastRequestedScope;
10469
+ let lastRequestedAt;
10213
10470
  async function fetchCheckpoint() {
10471
+ if (inFlight || document.visibilityState !== "visible") return null;
10472
+ const requestScope = config.getScope?.();
10473
+ if (requestScope === lastRequestedScope && lastRequestedAt !== void 0 && Date.now() - lastRequestedAt < pollMs)
10474
+ return null;
10475
+ inFlight = true;
10476
+ lastRequestedScope = requestScope;
10477
+ lastRequestedAt = Date.now();
10214
10478
  try {
10215
10479
  const res = await config.api.get(config.checkpointPath);
10216
10480
  if (!res.ok) return null;
10217
- return await res.json();
10481
+ const data = await res.json();
10482
+ if (requestScope !== config.getScope?.()) return null;
10483
+ if (scope !== requestScope) {
10484
+ primed = false;
10485
+ scope = requestScope;
10486
+ }
10487
+ return data;
10218
10488
  } catch {
10219
10489
  return null;
10220
- }
10221
- }
10222
- function notify(topic) {
10223
- for (const sub of subscribers) {
10224
- if (!sub.topic || sub.topic === topic) {
10225
- if (lastKnown[topic] > sub.lastSync) {
10226
- sub.lastSync = lastKnown[topic];
10227
- sub.callback();
10228
- }
10229
- }
10490
+ } finally {
10491
+ inFlight = false;
10230
10492
  }
10231
10493
  }
10232
10494
  function checkAndNotify(data) {
10233
- const tasks = data.tasks_synced_at || 0;
10234
- const time = data.time_synced_at || 0;
10235
- const integration = data.integration_synced_at || 0;
10236
- if (tasks > lastKnown.tasks) {
10237
- lastKnown.tasks = tasks;
10238
- notify("tasks");
10239
- }
10240
- if (time > lastKnown.time) {
10241
- lastKnown.time = time;
10242
- notify("time");
10243
- }
10244
- if (integration > lastKnown.integration) {
10245
- lastKnown.integration = integration;
10246
- notify("integration");
10495
+ const next = {
10496
+ tasks: data.tasks_synced_at || 0,
10497
+ time: data.time_synced_at || 0,
10498
+ integration: data.integration_synced_at || 0
10499
+ };
10500
+ const changed = ["tasks", "time", "integration"].filter(
10501
+ (topic) => primed && next[topic] > lastKnown[topic]
10502
+ );
10503
+ Object.assign(lastKnown, next);
10504
+ primed = true;
10505
+ for (const sub of subscribers) {
10506
+ if (changed.some((topic) => !sub.topic || sub.topic === topic))
10507
+ sub.callback();
10247
10508
  }
10248
10509
  }
10249
10510
  function startPolling() {
@@ -10270,8 +10531,7 @@ function createCrossAppSync(config) {
10270
10531
  };
10271
10532
  const sub = {
10272
10533
  topic,
10273
- callback: onSync,
10274
- lastSync: lastKnown[topic || "tasks"]
10534
+ callback: onSync
10275
10535
  };
10276
10536
  subscribers.push(sub);
10277
10537
  pollListeners += 1;
@@ -10342,6 +10602,80 @@ function createLiveRefresh(sync) {
10342
10602
  return { subscribeLiveRefresh, hasPendingCrossAppSync };
10343
10603
  }
10344
10604
 
10605
+ // src/sync/use-suite-revisions.ts
10606
+ import { useEffect, useRef } from "react";
10607
+ function nextRevisionTick(seen, ready, data) {
10608
+ if (!data) return { seen, ready, changed: [] };
10609
+ const next = { ...seen };
10610
+ if (!ready) {
10611
+ for (const [topic, value] of Object.entries(data)) {
10612
+ if (typeof value === "number") next[topic] = value;
10613
+ }
10614
+ return { seen: next, ready: true, changed: [] };
10615
+ }
10616
+ const changed = [];
10617
+ for (const [topic, value] of Object.entries(data)) {
10618
+ if (typeof value !== "number" || next[topic] === value) continue;
10619
+ next[topic] = value;
10620
+ changed.push(topic);
10621
+ }
10622
+ return { seen: next, ready: true, changed };
10623
+ }
10624
+ function useSuiteRevisions(options) {
10625
+ const loadRef = useRef(options.load);
10626
+ const onTopicRef = useRef(options.onTopic);
10627
+ loadRef.current = options.load;
10628
+ onTopicRef.current = options.onTopic;
10629
+ const pollMs = options.pollMs ?? 15e3;
10630
+ const scope = options.scope ?? "";
10631
+ const seenScope = useRef(null);
10632
+ useEffect(() => {
10633
+ if (!options.enabled || typeof window === "undefined") return;
10634
+ const switching = seenScope.current !== null && seenScope.current !== scope;
10635
+ seenScope.current = scope;
10636
+ let seen = {};
10637
+ let ready = switching;
10638
+ let timer;
10639
+ let inFlight = false;
10640
+ let disposed = false;
10641
+ const tick = async () => {
10642
+ if (inFlight || disposed) return;
10643
+ inFlight = true;
10644
+ try {
10645
+ const data = await loadRef.current();
10646
+ if (disposed) return;
10647
+ const step = nextRevisionTick(seen, ready, data);
10648
+ seen = step.seen;
10649
+ ready = step.ready;
10650
+ for (const topic of step.changed) onTopicRef.current(topic);
10651
+ } catch {
10652
+ } finally {
10653
+ inFlight = false;
10654
+ }
10655
+ };
10656
+ const stop = () => {
10657
+ if (timer !== void 0) window.clearInterval(timer);
10658
+ timer = void 0;
10659
+ };
10660
+ const start = () => {
10661
+ if (timer !== void 0) return;
10662
+ void tick();
10663
+ timer = window.setInterval(() => void tick(), pollMs);
10664
+ };
10665
+ const onVisibility = () => {
10666
+ if (document.visibilityState === "visible") start();
10667
+ else stop();
10668
+ };
10669
+ if (document.visibilityState === "visible") start();
10670
+ document.addEventListener("visibilitychange", onVisibility);
10671
+ return () => {
10672
+ disposed = true;
10673
+ stop();
10674
+ document.removeEventListener("visibilitychange", onVisibility);
10675
+ };
10676
+ }, [options.enabled, pollMs, scope]);
10677
+ }
10678
+
10345
10679
  // src/notifications/create-notifications-client.ts
10346
10680
  function createNotificationsClient(config) {
10347
10681
  const { api, parseApiError } = config;
@@ -10487,6 +10821,47 @@ function createWebPushClient(config) {
10487
10821
  return { supported, getState, enable, disable };
10488
10822
  }
10489
10823
 
10824
+ // src/notifications/use-email-notification-preference.ts
10825
+ import { useCallback, useEffect as useEffect2, useState } from "react";
10826
+ var EMAIL_PREFERENCE_PATH = "/api/notifications/email/";
10827
+ function useEmailNotificationPreference(api, parseApiError) {
10828
+ const [enabled, setEnabled] = useState(false);
10829
+ const [busy, setBusy] = useState(false);
10830
+ const [error, setError] = useState("");
10831
+ useEffect2(() => {
10832
+ let cancelled = false;
10833
+ api.get(EMAIL_PREFERENCE_PATH).then(async (res) => {
10834
+ if (!res.ok || cancelled) return;
10835
+ const data = await res.json();
10836
+ if (!cancelled) setEnabled(Boolean(data.enabled));
10837
+ }).catch(() => {
10838
+ });
10839
+ return () => {
10840
+ cancelled = true;
10841
+ };
10842
+ }, [api]);
10843
+ const onChange = useCallback(
10844
+ (next) => {
10845
+ setBusy(true);
10846
+ setError("");
10847
+ setEnabled(next);
10848
+ api.patch(EMAIL_PREFERENCE_PATH, { enabled: next }).then(async (res) => {
10849
+ if (!res.ok) {
10850
+ setEnabled(!next);
10851
+ setError(await parseApiError(res));
10852
+ }
10853
+ }).catch((err) => {
10854
+ setEnabled(!next);
10855
+ setError(
10856
+ err instanceof Error ? err.message : "Could not save email notifications."
10857
+ );
10858
+ }).finally(() => setBusy(false));
10859
+ },
10860
+ [api, parseApiError]
10861
+ );
10862
+ return { enabled, busy, error, onChange };
10863
+ }
10864
+
10490
10865
  // src/ai/suite-ai-job.ts
10491
10866
  function activeSuiteAiJobs(jobs) {
10492
10867
  return jobs.filter(
@@ -10568,12 +10943,12 @@ function createJsonLd(config) {
10568
10943
  }
10569
10944
 
10570
10945
  // src/hooks/use-is-night.ts
10571
- import { useEffect, useState } from "react";
10946
+ import { useEffect as useEffect3, useState as useState2 } from "react";
10572
10947
  var NIGHT_START_HOUR = 18;
10573
10948
  var NIGHT_END_HOUR = 6;
10574
10949
  function useIsNight() {
10575
- const [isNight, setIsNight] = useState(false);
10576
- useEffect(() => {
10950
+ const [isNight, setIsNight] = useState2(false);
10951
+ useEffect3(() => {
10577
10952
  const check = () => {
10578
10953
  const hour = (/* @__PURE__ */ new Date()).getHours();
10579
10954
  return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;
@@ -10597,6 +10972,8 @@ export {
10597
10972
  ERROR_RETRYABLE,
10598
10973
  FREE_PLAN_SEAT_HELP,
10599
10974
  ORGANISATION_SEAT_HELP,
10975
+ SUITE_APP_QUERY_CACHES,
10976
+ SUITE_QUERY_CACHE_MAX_AGE_MS,
10600
10977
  TIDES_ACCESS_HELP,
10601
10978
  TURTLETIME_ACCESS_HELP,
10602
10979
  WEBHOOK_ACTIVITY_ACTIONS,
@@ -10624,9 +11001,12 @@ export {
10624
11001
  formatAppError,
10625
11002
  hasAppAccess,
10626
11003
  isErrorCode,
11004
+ nextRevisionTick,
10627
11005
  normalizeError,
10628
11006
  normalizeThrown,
11007
+ persistSuiteQueries,
10629
11008
  planSeatSummary,
11009
+ readJsonSnapshot,
10630
11010
  resolveSubscriptionTier,
10631
11011
  seatTypeDescription,
10632
11012
  seatTypeFromFlags,
@@ -10634,10 +11014,14 @@ export {
10634
11014
  selectedToWebhookActions,
10635
11015
  sortedSuiteAiEvents,
10636
11016
  suiteAiEventSummary,
11017
+ suiteCacheBuster,
10637
11018
  tierAllowsTidesOnlySeats,
10638
11019
  tierAllowsTurtletime,
10639
11020
  tierLabel,
11021
+ useEmailNotificationPreference,
10640
11022
  useIsNight,
10641
- webhookActionsToSelected
11023
+ useSuiteRevisions,
11024
+ webhookActionsToSelected,
11025
+ writeJsonSnapshot
10642
11026
  };
10643
11027
  //# sourceMappingURL=index.mjs.map