@peers-app/peers-ui 0.26.6 → 0.27.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.
Files changed (44) hide show
  1. package/dist/hooks/index.d.ts +7 -0
  2. package/dist/hooks/index.js +24 -0
  3. package/dist/hooks/use-observable-state.d.ts +12 -0
  4. package/dist/hooks/use-observable-state.js +15 -0
  5. package/dist/hooks/use-observable-state.test.js +64 -0
  6. package/dist/hooks/use-observable.d.ts +8 -0
  7. package/dist/hooks/use-observable.js +35 -0
  8. package/dist/hooks/use-observable.test.d.ts +1 -0
  9. package/dist/{hooks.test.js → hooks/use-observable.test.js} +22 -62
  10. package/dist/hooks/use-on-screen.d.ts +3 -0
  11. package/dist/hooks/use-on-screen.js +23 -0
  12. package/dist/hooks/use-promise.d.ts +9 -0
  13. package/dist/hooks/use-promise.js +31 -0
  14. package/dist/hooks/use-rows.d.ts +42 -0
  15. package/dist/hooks/use-rows.js +83 -0
  16. package/dist/hooks/use-rows.test.d.ts +1 -0
  17. package/dist/hooks/use-rows.test.js +175 -0
  18. package/dist/hooks/use-subscription.d.ts +10 -0
  19. package/dist/hooks/use-subscription.js +21 -0
  20. package/dist/hooks/use-tool.d.ts +43 -0
  21. package/dist/hooks/use-tool.js +82 -0
  22. package/dist/hooks/use-tool.test.d.ts +1 -0
  23. package/dist/hooks/use-tool.test.js +190 -0
  24. package/dist/screens/tools/tool-catalog-ipc.d.ts +6 -54
  25. package/dist/utils.d.ts +9 -4
  26. package/dist/utils.js +9 -174
  27. package/package.json +3 -3
  28. package/src/hooks/index.ts +8 -0
  29. package/src/hooks/use-observable-state.test.tsx +101 -0
  30. package/src/hooks/use-observable-state.ts +29 -0
  31. package/src/{hooks.test.tsx → hooks/use-observable.test.tsx} +27 -100
  32. package/src/hooks/use-observable.ts +67 -0
  33. package/src/hooks/use-on-screen.ts +24 -0
  34. package/src/hooks/use-promise.ts +34 -0
  35. package/src/hooks/use-rows.test.tsx +196 -0
  36. package/src/hooks/use-rows.ts +120 -0
  37. package/src/hooks/use-subscription.ts +24 -0
  38. package/src/hooks/use-tool.test.tsx +225 -0
  39. package/src/hooks/use-tool.ts +116 -0
  40. package/src/utils.ts +9 -167
  41. package/dist/hooks.d.ts +0 -36
  42. package/dist/hooks.js +0 -108
  43. package/src/hooks.ts +0 -150
  44. /package/dist/{hooks.test.d.ts → hooks/use-observable-state.test.d.ts} +0 -0
@@ -0,0 +1,7 @@
1
+ export * from "./use-observable";
2
+ export * from "./use-observable-state";
3
+ export * from "./use-on-screen";
4
+ export * from "./use-promise";
5
+ export * from "./use-rows";
6
+ export * from "./use-subscription";
7
+ export * from "./use-tool";
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // `use-host-contract` stays internal: its screens import it by path.
18
+ __exportStar(require("./use-observable"), exports);
19
+ __exportStar(require("./use-observable-state"), exports);
20
+ __exportStar(require("./use-on-screen"), exports);
21
+ __exportStar(require("./use-promise"), exports);
22
+ __exportStar(require("./use-rows"), exports);
23
+ __exportStar(require("./use-subscription"), exports);
24
+ __exportStar(require("./use-tool"), exports);
@@ -0,0 +1,12 @@
1
+ import { type Observable } from "@peers-app/peers-sdk";
2
+ /**
3
+ * Use this to create an observable in a functional component.
4
+ * This automatically subscribes to the observable and triggers a re-render when the value changes.
5
+ * @param initialValue This will be the initial value of the observable
6
+ * @param doNotSubscribe If true, the observable will not be subscribed to automatically.
7
+ * This is useful if you want to create an observable that persists between rerenders but doesn't cause rerenders itself.
8
+ * @returns The same observable on every render.
9
+ */
10
+ export declare function useObservableState<T>(initialValue: T, doNotSubscribe?: boolean): Observable<T>;
11
+ /** Create an observable that may start undefined and subscribe unless `doNotSubscribe` is true. */
12
+ export declare function useObservableState<T = undefined>(initialValue?: T, doNotSubscribe?: boolean): Observable<T | undefined>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useObservableState = useObservableState;
4
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ const react_1 = require("react");
6
+ const use_observable_1 = require("./use-observable");
7
+ /** Create and optionally subscribe to a component-owned observable. */
8
+ function useObservableState(initialValue, doNotSubscribe) {
9
+ const [obs] = (0, react_1.useState)(() => initialValue !== undefined ? (0, peers_sdk_1.observable)(initialValue) : (0, peers_sdk_1.observable)());
10
+ if (!doNotSubscribe) {
11
+ // biome-ignore lint/correctness/useHookAtTopLevel: when doNotSubscribe, caller keeps a stable observable without React-driven subscription (see docstring).
12
+ (0, use_observable_1.useObservable)(obs);
13
+ }
14
+ return obs;
15
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const react_1 = require("@testing-library/react");
5
+ const use_observable_1 = require("./use-observable");
6
+ const use_observable_state_1 = require("./use-observable-state");
7
+ // Helper component to test useObservableState
8
+ function ObservableStateTestComponent({ initialValue }) {
9
+ const obs = (0, use_observable_state_1.useObservableState)(initialValue);
10
+ const [value] = (0, use_observable_1.useObservable)(obs);
11
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "state-value", children: value }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "increment", onClick: () => obs(obs() + 1), children: "Increment" })] }));
12
+ }
13
+ describe("useObservableState", () => {
14
+ it("should create an observable with the initial value", () => {
15
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ObservableStateTestComponent, { initialValue: 42 }));
16
+ expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("42");
17
+ });
18
+ it("should re-render when the observable is updated", async () => {
19
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ObservableStateTestComponent, { initialValue: 0 }));
20
+ expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("0");
21
+ (0, react_1.act)(() => {
22
+ react_1.screen.getByTestId("increment").click();
23
+ });
24
+ await (0, react_1.waitFor)(() => {
25
+ expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("1");
26
+ });
27
+ });
28
+ it("should maintain the same observable instance across re-renders", async () => {
29
+ let capturedObs = null;
30
+ function CaptureObsComponent() {
31
+ const obs = (0, use_observable_state_1.useObservableState)(0);
32
+ if (!capturedObs) {
33
+ capturedObs = obs;
34
+ }
35
+ const [value] = (0, use_observable_1.useObservable)(obs);
36
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "value", children: value }), (0, jsx_runtime_1.jsx)("span", { "data-testid": "same-obs", children: obs === capturedObs ? "same" : "different" }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "update", onClick: () => obs(obs() + 1), children: "Update" })] }));
37
+ }
38
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(CaptureObsComponent, {}));
39
+ expect(react_1.screen.getByTestId("same-obs")).toHaveTextContent("same");
40
+ // Trigger a re-render by updating the observable
41
+ (0, react_1.act)(() => {
42
+ react_1.screen.getByTestId("update").click();
43
+ });
44
+ await (0, react_1.waitFor)(() => {
45
+ expect(react_1.screen.getByTestId("value")).toHaveTextContent("1");
46
+ expect(react_1.screen.getByTestId("same-obs")).toHaveTextContent("same");
47
+ });
48
+ });
49
+ it("should not subscribe when doNotSubscribe is true", () => {
50
+ function NoSubscribeComponent() {
51
+ const obs = (0, use_observable_state_1.useObservableState)(0, true); // doNotSubscribe = true
52
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "value", children: obs() }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "update", onClick: () => obs(obs() + 1), children: "Update" })] }));
53
+ }
54
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(NoSubscribeComponent, {}));
55
+ expect(react_1.screen.getByTestId("value")).toHaveTextContent("0");
56
+ // Update won't cause re-render since we're not subscribed
57
+ (0, react_1.act)(() => {
58
+ react_1.screen.getByTestId("update").click();
59
+ });
60
+ // Value in DOM should still show 0 since component didn't re-render
61
+ // But the observable itself has been updated
62
+ expect(react_1.screen.getByTestId("value")).toHaveTextContent("0");
63
+ });
64
+ });
@@ -0,0 +1,8 @@
1
+ import { type Observable, type ReadonlyObservable } from "@peers-app/peers-sdk";
2
+ import type React from "react";
3
+ /** Read and subscribe to a writable observable, with a setter for its value. */
4
+ export declare function useObservable<T>(sub: Observable<T>, deps?: React.DependencyList): [T, (value: T) => void];
5
+ /** Read and subscribe to a read-only observable; its setter accepts no values. */
6
+ export declare function useObservable<T>(sub: ReadonlyObservable<T>, deps?: React.DependencyList): [T, (value: never) => void];
7
+ /** Read a plain value or a writable observable, retaining the existing optional-observable API. */
8
+ export declare function useObservable<T>(sub: Observable<T> | T, deps?: React.DependencyList): [T, (value: T) => void];
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useObservable = useObservable;
4
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ const react_1 = require("react");
6
+ /**
7
+ * Subscribe to an observable or computed in a functional component, using useSyncExternalStore
8
+ * for React 18 concurrent rendering. Only writable observables have a callable typed setter.
9
+ * @param sub the observable, computed, or plain value to read
10
+ * @param deps dependencies that cause re-subscription when changed
11
+ */
12
+ function useObservable(sub, deps = []) {
13
+ // Create memoized subscribe function that adapts Observable's subscribe API to useSyncExternalStore's expected signature
14
+ const subscribe = (0, react_1.useCallback)((onStoreChange) => {
15
+ if (!(0, peers_sdk_1.isSubscribable)(sub)) {
16
+ // If not subscribable, return a no-op unsubscribe
17
+ return () => { };
18
+ }
19
+ const subscription = sub.subscribe(onStoreChange);
20
+ return () => subscription.dispose();
21
+ }, [sub, ...deps]);
22
+ // getSnapshot returns the current value from the observable
23
+ const getSnapshot = (0, react_1.useCallback)(() => {
24
+ return (0, peers_sdk_1.unwrapObservable)(sub);
25
+ }, [sub, ...deps]);
26
+ // Use useSyncExternalStore for concurrent-safe subscription
27
+ const data = (0, react_1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
28
+ // Create setter that updates both local understanding and the observable
29
+ const setter = (0, react_1.useCallback)((newData) => {
30
+ if ((0, peers_sdk_1.isSubscribable)(sub)) {
31
+ sub(newData);
32
+ }
33
+ }, [sub]);
34
+ return [data, setter];
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -3,24 +3,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const peers_sdk_1 = require("@peers-app/peers-sdk");
5
5
  const react_1 = require("@testing-library/react");
6
- const hooks_1 = require("./hooks");
6
+ const use_observable_1 = require("./use-observable");
7
7
  // Helper component to test useObservable
8
8
  function TestComponent({ obs, testId = "value" }) {
9
- const [value, setValue] = (0, hooks_1.useObservable)(obs);
9
+ const [value, setValue] = (0, use_observable_1.useObservable)(obs);
10
10
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": testId, children: JSON.stringify(value) }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "setter", onClick: () => setValue("new-value"), children: "Set" })] }));
11
11
  }
12
12
  // Helper component to test array behavior
13
13
  function ArrayTestComponent({ obs }) {
14
- const [value] = (0, hooks_1.useObservable)(obs);
14
+ const [value] = (0, use_observable_1.useObservable)(obs);
15
15
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "array-value", children: JSON.stringify(value) }), (0, jsx_runtime_1.jsx)("span", { "data-testid": "array-length", children: value.length })] }));
16
16
  }
17
- // Helper component to test useObservableState
18
- function ObservableStateTestComponent({ initialValue }) {
19
- const obs = (0, hooks_1.useObservableState)(initialValue);
20
- const [value] = (0, hooks_1.useObservable)(obs);
21
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "state-value", children: value }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "increment", onClick: () => obs(obs() + 1), children: "Increment" })] }));
22
- }
23
17
  describe("useObservable", () => {
18
+ it("reads and follows a read-only observable without allowing typed writes", () => {
19
+ const source = (0, peers_sdk_1.observable)(1);
20
+ const view = source;
21
+ const { result } = (0, react_1.renderHook)(() => (0, use_observable_1.useObservable)(view));
22
+ const checkReadonly = () => {
23
+ // @ts-expect-error the read-only hook's setter accepts no values
24
+ result.current[1](2);
25
+ if ((0, peers_sdk_1.isSubscribable)(view)) {
26
+ // @ts-expect-error testing subscribability must not make a read-only view writable
27
+ view(2);
28
+ }
29
+ };
30
+ void checkReadonly;
31
+ const initial = result.current[0];
32
+ expect(initial).toBe(1);
33
+ (0, react_1.act)(() => source(3));
34
+ expect(result.current[0]).toBe(3);
35
+ });
24
36
  describe("basic functionality", () => {
25
37
  it("should return the current value of an observable", () => {
26
38
  const obs = (0, peers_sdk_1.observable)("hello");
@@ -162,7 +174,7 @@ describe("useObservable", () => {
162
174
  it("should handle rapid sequential updates correctly", async () => {
163
175
  const obs = (0, peers_sdk_1.observable)(0);
164
176
  function CounterComponent() {
165
- const [value] = (0, hooks_1.useObservable)(obs);
177
+ const [value] = (0, use_observable_1.useObservable)(obs);
166
178
  return (0, jsx_runtime_1.jsx)("span", { "data-testid": "counter", children: value });
167
179
  }
168
180
  (0, react_1.render)((0, jsx_runtime_1.jsx)(CounterComponent, {}));
@@ -177,55 +189,3 @@ describe("useObservable", () => {
177
189
  });
178
190
  });
179
191
  });
180
- describe("useObservableState", () => {
181
- it("should create an observable with the initial value", () => {
182
- (0, react_1.render)((0, jsx_runtime_1.jsx)(ObservableStateTestComponent, { initialValue: 42 }));
183
- expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("42");
184
- });
185
- it("should re-render when the observable is updated", async () => {
186
- (0, react_1.render)((0, jsx_runtime_1.jsx)(ObservableStateTestComponent, { initialValue: 0 }));
187
- expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("0");
188
- (0, react_1.act)(() => {
189
- react_1.screen.getByTestId("increment").click();
190
- });
191
- await (0, react_1.waitFor)(() => {
192
- expect(react_1.screen.getByTestId("state-value")).toHaveTextContent("1");
193
- });
194
- });
195
- it("should maintain the same observable instance across re-renders", async () => {
196
- let capturedObs = null;
197
- function CaptureObsComponent() {
198
- const obs = (0, hooks_1.useObservableState)(0);
199
- if (!capturedObs) {
200
- capturedObs = obs;
201
- }
202
- const [value] = (0, hooks_1.useObservable)(obs);
203
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "value", children: value }), (0, jsx_runtime_1.jsx)("span", { "data-testid": "same-obs", children: obs === capturedObs ? "same" : "different" }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "update", onClick: () => obs(obs() + 1), children: "Update" })] }));
204
- }
205
- (0, react_1.render)((0, jsx_runtime_1.jsx)(CaptureObsComponent, {}));
206
- expect(react_1.screen.getByTestId("same-obs")).toHaveTextContent("same");
207
- // Trigger a re-render by updating the observable
208
- (0, react_1.act)(() => {
209
- react_1.screen.getByTestId("update").click();
210
- });
211
- await (0, react_1.waitFor)(() => {
212
- expect(react_1.screen.getByTestId("value")).toHaveTextContent("1");
213
- expect(react_1.screen.getByTestId("same-obs")).toHaveTextContent("same");
214
- });
215
- });
216
- it("should not subscribe when doNotSubscribe is true", () => {
217
- function NoSubscribeComponent() {
218
- const obs = (0, hooks_1.useObservableState)(0, true); // doNotSubscribe = true
219
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-testid": "value", children: obs() }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "update", onClick: () => obs(obs() + 1), children: "Update" })] }));
220
- }
221
- (0, react_1.render)((0, jsx_runtime_1.jsx)(NoSubscribeComponent, {}));
222
- expect(react_1.screen.getByTestId("value")).toHaveTextContent("0");
223
- // Update won't cause re-render since we're not subscribed
224
- (0, react_1.act)(() => {
225
- react_1.screen.getByTestId("update").click();
226
- });
227
- // Value in DOM should still show 0 since component didn't re-render
228
- // But the observable itself has been updated
229
- expect(react_1.screen.getByTestId("value")).toHaveTextContent("0");
230
- });
231
- });
@@ -0,0 +1,3 @@
1
+ import type React from "react";
2
+ /** Track whether the referenced element intersects the viewport. */
3
+ export declare function useOnScreen(ref: React.RefObject<Element | null>): boolean;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useOnScreen = useOnScreen;
4
+ const react_1 = require("react");
5
+ /** Track whether the referenced element intersects the viewport. */
6
+ function useOnScreen(ref) {
7
+ const [isIntersecting, setIntersecting] = (0, react_1.useState)(false);
8
+ // Read ref.current once on mount (ref object identity is stable).
9
+ (0, react_1.useEffect)(() => {
10
+ const element = ref.current;
11
+ if (!element) {
12
+ return;
13
+ }
14
+ const observer = new IntersectionObserver(([entry]) => {
15
+ setIntersecting(entry.isIntersecting);
16
+ });
17
+ observer.observe(element);
18
+ return () => {
19
+ observer.disconnect();
20
+ };
21
+ }, []);
22
+ return isIntersecting;
23
+ }
@@ -0,0 +1,9 @@
1
+ import type React from "react";
2
+ /**
3
+ * Use this to easily wait for a promise in a functional component.
4
+ * @param p The promise to wait for
5
+ * @param initialValue the initial value to return before the promise resolves
6
+ * @param deps the dependencies to pass to useEffect which will trigger a re-render when any of the dependencies change
7
+ * @returns the initial value and the resolved value of the promise
8
+ */
9
+ export declare function usePromise<T>(p: Promise<T> | (() => Promise<T>), initialValue?: T, deps?: React.DependencyList): T | undefined;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePromise = usePromise;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Use this to easily wait for a promise in a functional component.
7
+ * @param p The promise to wait for
8
+ * @param initialValue the initial value to return before the promise resolves
9
+ * @param deps the dependencies to pass to useEffect which will trigger a re-render when any of the dependencies change
10
+ * @returns the initial value and the resolved value of the promise
11
+ */
12
+ function usePromise(p, initialValue, deps = []) {
13
+ const [data, setData] = (0, react_1.useState)(initialValue);
14
+ // `deps` drives when the effect re-runs; callers include `p` when it changes. Omitting `p` avoids duplicate tracking with `[...deps]`.
15
+ (0, react_1.useEffect)(() => {
16
+ let disposed = false;
17
+ if (typeof p === "function") {
18
+ p = p();
19
+ }
20
+ p.then((newData) => {
21
+ // if (!_.isEqual(newData, data) && !disposed) {
22
+ if (!disposed) {
23
+ setData(newData);
24
+ }
25
+ });
26
+ return () => {
27
+ disposed = true;
28
+ };
29
+ }, [...deps]);
30
+ return data;
31
+ }
@@ -0,0 +1,42 @@
1
+ import type { DataFilter, IDataQueryParams, IHostSubscribeOptions, ISubscriptionResult } from "@peers-app/peers-sdk";
2
+ /** A table `useRows` can read: a `hostContract` table, or anything with the same two members. */
3
+ export interface IRowsSource<T extends Record<string, any>> {
4
+ /** Rows matching `filter`, with paging and sorting options. */
5
+ list(filter?: DataFilter<T>, options?: IDataQueryParams<T>): Promise<T[]>;
6
+ /**
7
+ * Changes to the rows. Optional `ready` acknowledges remote attachment; `onReconnect` runs
8
+ * once a replacement subscription is live, when changes may have been missed.
9
+ */
10
+ readonly dataChanged: {
11
+ subscribe(handler: (event: unknown) => void, options?: IHostSubscribeOptions): ISubscriptionResult & {
12
+ readonly ready?: Promise<boolean>;
13
+ };
14
+ };
15
+ }
16
+ /** What {@link useRows} returns. */
17
+ export interface IUseRowsResult<T> {
18
+ /** The latest rows; empty until the first read. */
19
+ rows: T[];
20
+ /** True until a read settles after mounting or after the query changed. */
21
+ loading: boolean;
22
+ /** Why the latest read failed; cleared by the next one that succeeds. */
23
+ error: Error | undefined;
24
+ /** Read again now. Resolves when the rows are current. */
25
+ reload(): Promise<void>;
26
+ }
27
+ /**
28
+ * Rows from a contract table, kept current: read on mount, and read again whenever the table
29
+ * changes, once an asynchronous subscription is live, after a reconnect, or when `filter` or
30
+ * `options` change (compared as JSON). Changes
31
+ * that arrive during a read cause one more read, not one per change.
32
+ *
33
+ * ```tsx
34
+ * const { rows: headlines, error } = useRows(news.tables.Headlines, {}, { pageSize: 100 });
35
+ * ```
36
+ *
37
+ * @param table - A table from a `hostContract` client. Keep it stable (make the client once, at
38
+ * module scope, as `hostContract` does): a new table reads and subscribes again.
39
+ * @param filter - Which rows (all when omitted).
40
+ * @param options - Paging and sorting.
41
+ */
42
+ export declare function useRows<T extends Record<string, any>>(table: IRowsSource<T>, filter?: DataFilter<T>, options?: IDataQueryParams<T>): IUseRowsResult<T>;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useRows = useRows;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Rows from a contract table, kept current: read on mount, and read again whenever the table
7
+ * changes, once an asynchronous subscription is live, after a reconnect, or when `filter` or
8
+ * `options` change (compared as JSON). Changes
9
+ * that arrive during a read cause one more read, not one per change.
10
+ *
11
+ * ```tsx
12
+ * const { rows: headlines, error } = useRows(news.tables.Headlines, {}, { pageSize: 100 });
13
+ * ```
14
+ *
15
+ * @param table - A table from a `hostContract` client. Keep it stable (make the client once, at
16
+ * module scope, as `hostContract` does): a new table reads and subscribes again.
17
+ * @param filter - Which rows (all when omitted).
18
+ * @param options - Paging and sorting.
19
+ */
20
+ function useRows(table, filter, options) {
21
+ const key = JSON.stringify([filter ?? {}, options ?? {}]);
22
+ const query = (0, react_1.useRef)({ filter, options });
23
+ query.current = { filter, options };
24
+ const [state, setState] = (0, react_1.useState)({
25
+ rows: [],
26
+ loading: true,
27
+ });
28
+ const reloadRef = (0, react_1.useRef)(async () => undefined);
29
+ // `key` stands for filter and options, which the reads take from `query`.
30
+ (0, react_1.useEffect)(() => {
31
+ let cancelled = false;
32
+ let again = false;
33
+ let reading;
34
+ let subscription;
35
+ setState((previous) => (previous.loading ? previous : { ...previous, loading: true }));
36
+ const read = () => {
37
+ again = true;
38
+ reading ??= Promise.resolve().then(async () => {
39
+ try {
40
+ while (again && !cancelled) {
41
+ again = false;
42
+ try {
43
+ if (!subscription) {
44
+ subscription = table.dataChanged.subscribe(readAgain, { onReconnect: readAgain });
45
+ // Read again after remote attachment, covering changes missed during setup
46
+ // without delaying the initial read's connection timeout.
47
+ void subscription.ready?.then((live) => {
48
+ if (live && !cancelled)
49
+ readAgain();
50
+ });
51
+ }
52
+ const rows = await table.list(query.current.filter, query.current.options);
53
+ if (!cancelled) {
54
+ setState({ rows: Array.isArray(rows) ? rows : [], loading: false });
55
+ }
56
+ }
57
+ catch (cause) {
58
+ if (!cancelled) {
59
+ const error = cause instanceof Error ? cause : new Error(String(cause));
60
+ setState((previous) => ({ rows: previous.rows, loading: false, error }));
61
+ }
62
+ }
63
+ }
64
+ }
65
+ finally {
66
+ reading = undefined;
67
+ }
68
+ });
69
+ return reading;
70
+ };
71
+ const readAgain = () => {
72
+ void read();
73
+ };
74
+ reloadRef.current = read;
75
+ readAgain();
76
+ return () => {
77
+ cancelled = true;
78
+ subscription?.unsubscribe();
79
+ };
80
+ }, [table, key]);
81
+ const reload = (0, react_1.useCallback)(() => reloadRef.current(), []);
82
+ return { rows: state.rows, loading: state.loading, error: state.error, reload };
83
+ }
@@ -0,0 +1 @@
1
+ export {};