@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.
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +24 -0
- package/dist/hooks/use-observable-state.d.ts +12 -0
- package/dist/hooks/use-observable-state.js +15 -0
- package/dist/hooks/use-observable-state.test.js +64 -0
- package/dist/hooks/use-observable.d.ts +8 -0
- package/dist/hooks/use-observable.js +35 -0
- package/dist/hooks/use-observable.test.d.ts +1 -0
- package/dist/{hooks.test.js → hooks/use-observable.test.js} +22 -62
- package/dist/hooks/use-on-screen.d.ts +3 -0
- package/dist/hooks/use-on-screen.js +23 -0
- package/dist/hooks/use-promise.d.ts +9 -0
- package/dist/hooks/use-promise.js +31 -0
- package/dist/hooks/use-rows.d.ts +42 -0
- package/dist/hooks/use-rows.js +83 -0
- package/dist/hooks/use-rows.test.d.ts +1 -0
- package/dist/hooks/use-rows.test.js +175 -0
- package/dist/hooks/use-subscription.d.ts +10 -0
- package/dist/hooks/use-subscription.js +21 -0
- package/dist/hooks/use-tool.d.ts +43 -0
- package/dist/hooks/use-tool.js +82 -0
- package/dist/hooks/use-tool.test.d.ts +1 -0
- package/dist/hooks/use-tool.test.js +190 -0
- package/dist/screens/tools/tool-catalog-ipc.d.ts +6 -54
- package/dist/utils.d.ts +9 -4
- package/dist/utils.js +9 -174
- package/package.json +3 -3
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-observable-state.test.tsx +101 -0
- package/src/hooks/use-observable-state.ts +29 -0
- package/src/{hooks.test.tsx → hooks/use-observable.test.tsx} +27 -100
- package/src/hooks/use-observable.ts +67 -0
- package/src/hooks/use-on-screen.ts +24 -0
- package/src/hooks/use-promise.ts +34 -0
- package/src/hooks/use-rows.test.tsx +196 -0
- package/src/hooks/use-rows.ts +120 -0
- package/src/hooks/use-subscription.ts +24 -0
- package/src/hooks/use-tool.test.tsx +225 -0
- package/src/hooks/use-tool.ts +116 -0
- package/src/utils.ts +9 -167
- package/dist/hooks.d.ts +0 -36
- package/dist/hooks.js +0 -108
- package/src/hooks.ts +0 -150
- /package/dist/{hooks.test.d.ts → hooks/use-observable-state.test.d.ts} +0 -0
|
@@ -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
|
|
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,
|
|
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,
|
|
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,
|
|
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,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 {};
|