@tangentfeed/react 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sreeraj T A
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @tangentfeed/react
2
+
3
+ React hooks for tangentfeed.
4
+
5
+ ```tsx
6
+ const db = useSpace({ space: "kitchen-42", transports: [broadcast()] });
7
+ const { rows } = useRows(db, "tasks");
8
+ ```
9
+
10
+ Hooks: `useSpace`, `useRows`, `useRow`, `usePeers`, `useTable`.
11
+
12
+ Part of [tangentfeed](https://github.com/sreerajta/tangentfeed). MIT licensed.
@@ -0,0 +1,59 @@
1
+ import { SyncedSpace, OpenSpaceOptions } from 'tangentfeed';
2
+ import { RowData, Json } from '@tangentfeed/core';
3
+ import { SchemaShape, TableName, RowOf, InsertInput, UpdateInput } from '@tangentfeed/schema';
4
+
5
+ /**
6
+ * React bindings for tangentfeed.
7
+ *
8
+ * The engine is push-based (subscribe fires after every committed batch), so
9
+ * these hooks are thin: they re-read the affected slice and re-render. Reads
10
+ * are local and fast, but they are async, so each hook exposes a `loading`
11
+ * flag for the first paint.
12
+ *
13
+ * const db = useSpace({ space: "kitchen-42", transports: [broadcast()] });
14
+ * const { rows } = useRows(db, "tasks");
15
+ */
16
+
17
+ /**
18
+ * Each hook is generic over the schema so a typed space keeps its types
19
+ * through the binding. With no schema these collapse to the untyped forms the
20
+ * hooks had before, so existing components are unaffected.
21
+ */
22
+ type Schema = SchemaShape | undefined;
23
+ /** Table names accepted for a given schema. */
24
+ type TableArg<S extends Schema> = S extends SchemaShape ? TableName<S> : string;
25
+ /** A read row for a given schema and table. */
26
+ type RowType<S extends Schema, T> = S extends SchemaShape ? T extends keyof S ? RowOf<S, T> : never : RowData;
27
+ type InsertArg<S extends Schema, T> = S extends SchemaShape ? T extends keyof S ? InsertInput<S, T> : never : Record<string, Json>;
28
+ type UpdateArg<S extends Schema, T> = S extends SchemaShape ? T extends keyof S ? UpdateInput<S, T> : never : Record<string, Json>;
29
+ /**
30
+ * Open a space for the lifetime of the component. Returns null until ready.
31
+ *
32
+ * The options object is captured on first render; changing `space` opens a new
33
+ * database and closes the old one, other fields are ignored after mount (they
34
+ * describe how to connect, not what to display).
35
+ */
36
+ declare function useSpace<S extends Schema = undefined>(opts: OpenSpaceOptions<S>): SyncedSpace<S> | null;
37
+ /** Live view of every visible row in a table, sorted by rowId. */
38
+ declare function useRows<S extends Schema = undefined, T extends TableArg<S> = TableArg<S>>(db: SyncedSpace<S> | null, table: T): {
39
+ rows: RowType<S, T>[];
40
+ loading: boolean;
41
+ };
42
+ /** Live view of one row. `row` is undefined when absent or deleted. */
43
+ declare function useRow<S extends Schema = undefined, T extends TableArg<S> = TableArg<S>>(db: SyncedSpace<S> | null, table: T, rowId: string | null | undefined): {
44
+ row: RowType<S, T> | undefined;
45
+ loading: boolean;
46
+ };
47
+ /**
48
+ * Currently reachable peers. Polled, because transports report connection
49
+ * state through their own callbacks rather than the engine's change stream.
50
+ */
51
+ declare function usePeers<S extends Schema = undefined>(db: SyncedSpace<S> | null, intervalMs?: number): string[];
52
+ /** Stable mutation helpers bound to a table. */
53
+ declare function useTable<S extends Schema = undefined, T extends TableArg<S> = TableArg<S>>(db: SyncedSpace<S> | null, table: T): {
54
+ insert: (values: InsertArg<S, T>) => Promise<string>;
55
+ update: (row: string, values: UpdateArg<S, T>) => Promise<void>;
56
+ remove: (row: string) => Promise<void>;
57
+ };
58
+
59
+ export { usePeers, useRow, useRows, useSpace, useTable };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ // src/index.ts
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ openSpace
5
+ } from "tangentfeed";
6
+ function useSpace(opts) {
7
+ const [db, setDb] = useState(null);
8
+ const optsRef = useRef(opts);
9
+ optsRef.current = opts;
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ let opened = null;
13
+ const open = openSpace;
14
+ void open(optsRef.current).then((space) => {
15
+ if (cancelled) {
16
+ void space.close();
17
+ return;
18
+ }
19
+ opened = space;
20
+ setDb(space);
21
+ });
22
+ return () => {
23
+ cancelled = true;
24
+ setDb(null);
25
+ void opened?.close();
26
+ };
27
+ }, [opts.space]);
28
+ return db;
29
+ }
30
+ function useRows(db, table) {
31
+ const [rows, setRows] = useState([]);
32
+ const [loading, setLoading] = useState(true);
33
+ useEffect(() => {
34
+ if (!db) {
35
+ setRows([]);
36
+ setLoading(true);
37
+ return;
38
+ }
39
+ let live = true;
40
+ const plain = db;
41
+ const refresh = async () => {
42
+ const next = await plain.list(table);
43
+ if (live) {
44
+ setRows(next);
45
+ setLoading(false);
46
+ }
47
+ };
48
+ void refresh();
49
+ const unsub = plain.subscribe((ev) => {
50
+ if (ev.changes.some((c) => c.table === table)) void refresh();
51
+ });
52
+ return () => {
53
+ live = false;
54
+ unsub();
55
+ };
56
+ }, [db, table]);
57
+ return { rows, loading };
58
+ }
59
+ function useRow(db, table, rowId) {
60
+ const [row, setRow] = useState(void 0);
61
+ const [loading, setLoading] = useState(true);
62
+ useEffect(() => {
63
+ if (!db || !rowId) {
64
+ setRow(void 0);
65
+ setLoading(!!rowId);
66
+ return;
67
+ }
68
+ let live = true;
69
+ const plain = db;
70
+ const refresh = async () => {
71
+ const next = await plain.get(table, rowId);
72
+ if (live) {
73
+ setRow(next);
74
+ setLoading(false);
75
+ }
76
+ };
77
+ void refresh();
78
+ const unsub = plain.subscribe((ev) => {
79
+ if (ev.changes.some((c) => c.table === table && c.row === rowId)) void refresh();
80
+ });
81
+ return () => {
82
+ live = false;
83
+ unsub();
84
+ };
85
+ }, [db, table, rowId]);
86
+ return { row, loading };
87
+ }
88
+ function usePeers(db, intervalMs = 1e3) {
89
+ const [peers, setPeers] = useState([]);
90
+ useEffect(() => {
91
+ if (!db) {
92
+ setPeers([]);
93
+ return;
94
+ }
95
+ const tick = () => setPeers(db.peers());
96
+ tick();
97
+ const timer = setInterval(tick, intervalMs);
98
+ return () => clearInterval(timer);
99
+ }, [db, intervalMs]);
100
+ return peers;
101
+ }
102
+ function useTable(db, table) {
103
+ const insert = useCallback(
104
+ (values) => {
105
+ if (!db) throw new Error("space not ready");
106
+ return db.insert(table, values);
107
+ },
108
+ [db, table]
109
+ );
110
+ const update = useCallback(
111
+ (row, values) => {
112
+ if (!db) throw new Error("space not ready");
113
+ return db.update(table, row, values);
114
+ },
115
+ [db, table]
116
+ );
117
+ const remove = useCallback(
118
+ (row) => {
119
+ if (!db) throw new Error("space not ready");
120
+ return db.delete(table, row);
121
+ },
122
+ [db, table]
123
+ );
124
+ return useMemo(() => ({ insert, update, remove }), [insert, update, remove]);
125
+ }
126
+ export {
127
+ usePeers,
128
+ useRow,
129
+ useRows,
130
+ useSpace,
131
+ useTable
132
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@tangentfeed/react",
3
+ "version": "0.2.0",
4
+ "description": "React hooks for tangentfeed",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "test": "vitest run --passWithNoTests",
22
+ "build": "tsup src/index.ts --format esm --dts --clean --external react",
23
+ "prepack": "npm run build"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "@tangentfeed/core": "0.2.0",
30
+ "@tangentfeed/schema": "0.2.0",
31
+ "tangentfeed": "0.2.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^18.3.0",
35
+ "react": "^18.3.1",
36
+ "tsup": "^8.5.0",
37
+ "typescript": "^5.5.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/sreerajta/tangentfeed.git",
42
+ "directory": "packages/react"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }