@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 +21 -0
- package/README.md +12 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +132 -0
- package/package.json +47 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|