@protobase/ui 0.23.3 → 0.23.4

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/app/app.js CHANGED
@@ -12,11 +12,12 @@ import { CustomBoundary } from "./pages/custom-boundary.js";
12
12
  import { PluginSourceProvider } from "../plugins/plugin-source.js";
13
13
  import { AuthGate } from "../auth/auth-gate.js";
14
14
  import { isAdmin, userToShell } from "../auth/user-to-shell.js";
15
+ import { sourceStatusOf } from "../data/with-sources.js";
16
+ import { useLiveUpdates } from "../data/use-live-updates.js";
15
17
  import { useOpenedRecord } from "../data/use-record.js";
16
18
  import { humanize } from "../live/naming.js";
17
19
  import { ToastProvider } from "../toasts/toast-provider.js";
18
20
  import { recordTitle } from "../live/model-helpers.js";
19
- import { sourceStatusOf } from "../data/with-sources.js";
20
21
  import { MetaGate, Notice, useAdminMeta } from "./meta-gate.js";
21
22
  import { useAppAddress } from "./use-app-address.js";
22
23
  import { documentTitle, titleParts, useDocumentTitle } from "./document-title.js";
@@ -75,6 +76,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
75
76
  resource: shown?.resource,
76
77
  key: shown?.key
77
78
  });
79
+ useLiveUpdates(meta);
78
80
  const groups = navFromMeta(meta, basePath, recent);
79
81
  const firstPage = Object.keys(meta.pages)[0];
80
82
  const firstResource = Object.keys(meta.resources)[0];
@@ -1,10 +1,10 @@
1
1
  import { Spinner } from "../primitives/spinner.js";
2
2
  import { ClientProvider, useClient, useSourceClients } from "../data/api-provider.js";
3
+ import { withSources } from "../data/with-sources.js";
3
4
  import { MetaContext } from "../data/meta-context.js";
4
5
  import { allowEverything } from "../data/meta-data.js";
5
6
  import { useMeta } from "../data/use-meta.js";
6
7
  import { useSourceMetas } from "../data/use-source-metas.js";
7
- import { withSources } from "../data/with-sources.js";
8
8
  import { useContext, useMemo } from "react";
9
9
  import { isApiError, routedClient } from "@protobase/client";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -56,6 +56,7 @@ export declare const useClient: () => {
56
56
  form: (name: string) => Promise<import("@protobase/client").PublicForm>;
57
57
  formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
58
58
  submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
59
+ live: (handlers: import("@protobase/client").LiveHandlers) => () => void;
59
60
  };
60
61
  /**
61
62
  * The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
@@ -114,6 +115,7 @@ export declare const useResourceClient: (resource: string) => {
114
115
  form: (name: string) => Promise<import("@protobase/client").PublicForm>;
115
116
  formChallenge: (name: string) => Promise<import("@protobase/client").FormChallenge>;
116
117
  submitForm: (name: string, values: Record<string, unknown>, proof: import("@protobase/client").FormProof) => Promise<void>;
118
+ live: (handlers: import("@protobase/client").LiveHandlers) => () => void;
117
119
  };
118
120
  export type ApiProviderProps = {
119
121
  /** Defaults to `/api/v1` on the current origin. */
@@ -9,6 +9,8 @@ const createFocusRefresh = () => {
9
9
  let paused = 0;
10
10
  return {
11
11
  refetchOnWindowFocus: () => paused > 0 ? false : "always",
12
+ /** Whether a form on screen holds unsaved changes, so its record must not be read again under it. */
13
+ paused: () => paused > 0,
12
14
  /** Holds the refetches until the returned function is called. */
13
15
  pause: () => {
14
16
  paused += 1;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reads again what the page shows of the resources whose records changed: every query keyed by the resource, as after
3
+ * a write of the page's own. While a form holds unsaved changes (`paused`), the records stay as they are, only marked
4
+ * stale, so the form's Save still finds a conflict.
5
+ */
6
+ const liveRefresh = (queryClient, paused) => {
7
+ const pending = /* @__PURE__ */ new Set();
8
+ let timer;
9
+ const flush = () => {
10
+ timer = void 0;
11
+ const resources = new Set(pending);
12
+ pending.clear();
13
+ const shown = (key) => resources.has(key[1]);
14
+ queryClient.invalidateQueries({ predicate: (query) => shown(query.queryKey) && query.queryKey[0] !== "record" });
15
+ queryClient.invalidateQueries({
16
+ predicate: (query) => shown(query.queryKey) && query.queryKey[0] === "record",
17
+ refetchType: paused() ? "none" : "active"
18
+ });
19
+ };
20
+ return {
21
+ changed: (resources) => {
22
+ for (const resource of resources) pending.add(resource);
23
+ if (pending.size > 0 && !timer) timer = setTimeout(flush, 300);
24
+ },
25
+ stop: () => clearTimeout(timer)
26
+ };
27
+ };
28
+ //#endregion
29
+ export { liveRefresh };
@@ -0,0 +1,36 @@
1
+ import { FocusRefreshContext } from "./focus-refresh.js";
2
+ import { useClient } from "./api-provider.js";
3
+ import { liveRefresh } from "./live-refresh.js";
4
+ import { sourceStatusOf } from "./with-sources.js";
5
+ import { useContext, useEffect, useMemo } from "react";
6
+ import { useQueryClient } from "@tanstack/react-query";
7
+ //#region src/data/use-live-updates.ts
8
+ /**
9
+ * Keeps what the page shows of the app's `.live()` resources current: hears which records change (`client.live`) and
10
+ * reads again the lists, records and counts of their resources. Connects only when the caller reads a live resource.
11
+ */
12
+ const useLiveUpdates = (meta) => {
13
+ const client = useClient();
14
+ const queryClient = useQueryClient();
15
+ const focusRefresh = useContext(FocusRefreshContext);
16
+ const live = useMemo(() => Object.values(meta.resources).filter((model) => model.live && !sourceStatusOf(meta, model.name)).map((model) => model.name).sort().join(","), [meta]);
17
+ useEffect(() => {
18
+ if (!live) return;
19
+ const refresh = liveRefresh(queryClient, () => focusRefresh?.paused() ?? false);
20
+ const stop = client.live({
21
+ onChanges: (changes) => refresh.changed(changes.map((change) => change.resource)),
22
+ onResync: () => refresh.changed(live.split(","))
23
+ });
24
+ return () => {
25
+ stop();
26
+ refresh.stop();
27
+ };
28
+ }, [
29
+ client,
30
+ queryClient,
31
+ focusRefresh,
32
+ live
33
+ ]);
34
+ };
35
+ //#endregion
36
+ export { useLiveUpdates };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.23.3";
2
+ var version = "0.23.4";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };