@linkedrecords/react 0.5.0 → 0.7.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/dist/index.d.mts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +101 -53
- package/dist/index.mjs +99 -54
- package/package.json +3 -3
- package/src/LinkedRecordsProvider.tsx +4 -2
- package/src/index.tsx +2 -0
- package/src/queryKey.ts +6 -0
- package/src/useActorId.ts +24 -0
- package/src/useFacts.ts +39 -0
- package/src/useRecords.ts +73 -72
package/dist/index.d.mts
CHANGED
|
@@ -26,13 +26,33 @@ interface LinkedRecordsProviderProps {
|
|
|
26
26
|
}
|
|
27
27
|
declare function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
[key: string]:
|
|
31
|
-
}
|
|
32
|
-
|
|
29
|
+
type JSONValue = string | number | boolean | null | JSONValue[] | {
|
|
30
|
+
[key: string]: JSONValue;
|
|
31
|
+
};
|
|
32
|
+
type KVValue = {
|
|
33
|
+
[key: string]: JSONValue | undefined;
|
|
34
|
+
};
|
|
35
|
+
type KVRecord<T extends object = KVValue> = T & {
|
|
36
|
+
_id?: string;
|
|
37
|
+
_createdAt?: Date;
|
|
38
|
+
};
|
|
39
|
+
type CompoundRecordQuery = Parameters<LinkedRecords['Record']['subscribeToQuery']>[0];
|
|
40
|
+
type RecordQueryClause = Exclude<CompoundRecordQuery[string], string>[number];
|
|
41
|
+
declare function useKeyValueRecordsWithStatus<T extends object = KVValue>(query: RecordQueryClause[]): {
|
|
42
|
+
records: KVRecord<T>[];
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
};
|
|
45
|
+
declare function useKeyValueRecords<T extends object = KVValue>(query: RecordQueryClause[]): KVRecord<T>[];
|
|
33
46
|
/** @deprecated Use useKeyValueRecords instead. */
|
|
34
47
|
declare const useKeyValueAttributes: typeof useKeyValueRecords;
|
|
35
48
|
|
|
49
|
+
type FactQuery = Parameters<LinkedRecords['Fact']['findAll']>[0];
|
|
50
|
+
type Fact = Awaited<ReturnType<LinkedRecords['Fact']['findAll']>>[number];
|
|
51
|
+
declare function useFacts(query: FactQuery): {
|
|
52
|
+
facts: Fact[];
|
|
53
|
+
reload: () => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
36
56
|
declare function useLinkedRecords(): LinkedRecordsContextType;
|
|
37
57
|
|
|
38
58
|
type UserInfo = {
|
|
@@ -40,4 +60,6 @@ type UserInfo = {
|
|
|
40
60
|
};
|
|
41
61
|
declare function useUserInfo(): UserInfo | null;
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
declare function useActorId(): string | undefined;
|
|
64
|
+
|
|
65
|
+
export { type KVRecord, type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, type RecordQueryClause, useActorId, useFacts, useKeyValueAttributes, useKeyValueRecords, useKeyValueRecordsWithStatus, useLinkedRecords, useUserInfo };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,13 +26,33 @@ interface LinkedRecordsProviderProps {
|
|
|
26
26
|
}
|
|
27
27
|
declare function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
[key: string]:
|
|
31
|
-
}
|
|
32
|
-
|
|
29
|
+
type JSONValue = string | number | boolean | null | JSONValue[] | {
|
|
30
|
+
[key: string]: JSONValue;
|
|
31
|
+
};
|
|
32
|
+
type KVValue = {
|
|
33
|
+
[key: string]: JSONValue | undefined;
|
|
34
|
+
};
|
|
35
|
+
type KVRecord<T extends object = KVValue> = T & {
|
|
36
|
+
_id?: string;
|
|
37
|
+
_createdAt?: Date;
|
|
38
|
+
};
|
|
39
|
+
type CompoundRecordQuery = Parameters<LinkedRecords['Record']['subscribeToQuery']>[0];
|
|
40
|
+
type RecordQueryClause = Exclude<CompoundRecordQuery[string], string>[number];
|
|
41
|
+
declare function useKeyValueRecordsWithStatus<T extends object = KVValue>(query: RecordQueryClause[]): {
|
|
42
|
+
records: KVRecord<T>[];
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
};
|
|
45
|
+
declare function useKeyValueRecords<T extends object = KVValue>(query: RecordQueryClause[]): KVRecord<T>[];
|
|
33
46
|
/** @deprecated Use useKeyValueRecords instead. */
|
|
34
47
|
declare const useKeyValueAttributes: typeof useKeyValueRecords;
|
|
35
48
|
|
|
49
|
+
type FactQuery = Parameters<LinkedRecords['Fact']['findAll']>[0];
|
|
50
|
+
type Fact = Awaited<ReturnType<LinkedRecords['Fact']['findAll']>>[number];
|
|
51
|
+
declare function useFacts(query: FactQuery): {
|
|
52
|
+
facts: Fact[];
|
|
53
|
+
reload: () => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
36
56
|
declare function useLinkedRecords(): LinkedRecordsContextType;
|
|
37
57
|
|
|
38
58
|
type UserInfo = {
|
|
@@ -40,4 +60,6 @@ type UserInfo = {
|
|
|
40
60
|
};
|
|
41
61
|
declare function useUserInfo(): UserInfo | null;
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
declare function useActorId(): string | undefined;
|
|
64
|
+
|
|
65
|
+
export { type KVRecord, type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, type RecordQueryClause, useActorId, useFacts, useKeyValueAttributes, useKeyValueRecords, useKeyValueRecordsWithStatus, useLinkedRecords, useUserInfo };
|
package/dist/index.js
CHANGED
|
@@ -32,8 +32,11 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
LinkedRecordsContext: () => LinkedRecordsContext,
|
|
34
34
|
LinkedRecordsProvider: () => LinkedRecordsProvider,
|
|
35
|
+
useActorId: () => useActorId,
|
|
36
|
+
useFacts: () => useFacts,
|
|
35
37
|
useKeyValueAttributes: () => useKeyValueAttributes,
|
|
36
38
|
useKeyValueRecords: () => useKeyValueRecords,
|
|
39
|
+
useKeyValueRecordsWithStatus: () => useKeyValueRecordsWithStatus,
|
|
37
40
|
useLinkedRecords: () => useLinkedRecords,
|
|
38
41
|
useUserInfo: () => useUserInfo
|
|
39
42
|
});
|
|
@@ -63,11 +66,13 @@ function createClient(serverUrl, config) {
|
|
|
63
66
|
}
|
|
64
67
|
function LinkedRecordsProvider({ children, serverUrl, config }) {
|
|
65
68
|
const [lr] = (0, import_react2.useState)(() => config ? createClient(serverUrl, config) : import_browser.default.getPublicClient(serverUrl));
|
|
66
|
-
|
|
69
|
+
const value = (0, import_react2.useMemo)(() => ({ lr }), [lr]);
|
|
70
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedRecordsContext.Provider, { value, children });
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// src/useRecords.ts
|
|
70
74
|
var import_react4 = require("react");
|
|
75
|
+
var import_browser2 = require("@linkedrecords/browser");
|
|
71
76
|
|
|
72
77
|
// src/useLinkedRecords.ts
|
|
73
78
|
var import_react3 = require("react");
|
|
@@ -79,93 +84,136 @@ function useLinkedRecords() {
|
|
|
79
84
|
return context;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
// src/
|
|
83
|
-
var import_browser2 = require("@linkedrecords/browser");
|
|
87
|
+
// src/queryKey.ts
|
|
84
88
|
function getQueryKey(query) {
|
|
85
89
|
return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
|
|
86
90
|
}
|
|
87
|
-
|
|
91
|
+
|
|
92
|
+
// src/useRecords.ts
|
|
93
|
+
function useKeyValueRecordsWithStatus(query) {
|
|
88
94
|
const { lr } = useLinkedRecords();
|
|
89
95
|
const [records, setRecords] = (0, import_react4.useState)([]);
|
|
96
|
+
const [isLoading, setIsLoading] = (0, import_react4.useState)(true);
|
|
90
97
|
const queryKey = getQueryKey(query);
|
|
91
98
|
(0, import_react4.useEffect)(() => {
|
|
99
|
+
setIsLoading(true);
|
|
92
100
|
let cancelled = false;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const recordUnsubscribes = /* @__PURE__ */ new Map();
|
|
102
|
+
const toKVRecord = async (r) => ({
|
|
103
|
+
_id: r.id,
|
|
104
|
+
_createdAt: r.createdAt,
|
|
105
|
+
...await r.getValue()
|
|
106
|
+
});
|
|
107
|
+
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
108
|
+
records: [
|
|
109
|
+
["$it", "$hasDataType", import_browser2.KeyValueRecord],
|
|
110
|
+
...query
|
|
111
|
+
]
|
|
112
|
+
}, async ({ records: records2 }) => {
|
|
113
|
+
const kvRecords = records2;
|
|
114
|
+
const values = await Promise.all(kvRecords.map(toKVRecord));
|
|
115
|
+
if (cancelled) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setRecords(values);
|
|
119
|
+
setIsLoading(false);
|
|
120
|
+
const ids = new Set(kvRecords.map((r) => r.id));
|
|
121
|
+
recordUnsubscribes.forEach((unsubscribe, id) => {
|
|
122
|
+
if (!ids.has(id)) {
|
|
123
|
+
recordUnsubscribes.delete(id);
|
|
124
|
+
unsubscribe.then((fn) => fn());
|
|
103
125
|
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
108
|
-
records: [
|
|
109
|
-
["$it", "$hasDataType", import_browser2.KeyValueRecord],
|
|
110
|
-
...query
|
|
111
|
-
]
|
|
112
|
-
}, async ({ records: records2 }) => {
|
|
113
|
-
const values = await Promise.all(records2.map(async (r) => ({
|
|
114
|
-
_id: r.id,
|
|
115
|
-
...await r.getValue()
|
|
116
|
-
})));
|
|
117
|
-
if (cancelled) {
|
|
126
|
+
});
|
|
127
|
+
kvRecords.forEach((r) => {
|
|
128
|
+
if (recordUnsubscribes.has(r.id)) {
|
|
118
129
|
return;
|
|
119
130
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
131
|
+
recordUnsubscribes.set(r.id, r.subscribe(async () => {
|
|
132
|
+
const newValue = await toKVRecord(r);
|
|
133
|
+
if (cancelled) {
|
|
123
134
|
return;
|
|
124
135
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (cancelled) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const newValue = await r.getValue();
|
|
131
|
-
if (cancelled) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
setRecords((prev) => prev.map(
|
|
135
|
-
(v) => v._id === r.id ? { _id: r.id, ...newValue } : v
|
|
136
|
-
));
|
|
137
|
-
});
|
|
138
|
-
});
|
|
136
|
+
setRecords((prev) => prev.map((v) => v._id === r.id ? newValue : v));
|
|
137
|
+
}));
|
|
139
138
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
});
|
|
140
|
+
queryUnsubscribe.catch((err) => {
|
|
141
|
+
console.error("useKeyValueRecords: could not subscribe to query", err);
|
|
143
142
|
});
|
|
144
143
|
return () => {
|
|
145
144
|
cancelled = true;
|
|
146
|
-
|
|
145
|
+
queryUnsubscribe.then((unsubscribe) => unsubscribe(), () => {
|
|
146
|
+
});
|
|
147
|
+
recordUnsubscribes.forEach((unsubscribe) => unsubscribe.then((fn) => fn()));
|
|
147
148
|
};
|
|
148
149
|
}, [lr, queryKey]);
|
|
149
|
-
return records;
|
|
150
|
+
return { records, isLoading };
|
|
151
|
+
}
|
|
152
|
+
function useKeyValueRecords(query) {
|
|
153
|
+
return useKeyValueRecordsWithStatus(query).records;
|
|
150
154
|
}
|
|
151
155
|
var useKeyValueAttributes = useKeyValueRecords;
|
|
152
156
|
|
|
153
|
-
// src/
|
|
157
|
+
// src/useFacts.ts
|
|
154
158
|
var import_react5 = require("react");
|
|
155
|
-
function
|
|
159
|
+
function useFacts(query) {
|
|
156
160
|
const { lr } = useLinkedRecords();
|
|
157
|
-
const [
|
|
161
|
+
const [facts, setFacts] = (0, import_react5.useState)([]);
|
|
162
|
+
const [reloadCount, setReloadCount] = (0, import_react5.useState)(0);
|
|
163
|
+
const queryKey = getQueryKey(query);
|
|
158
164
|
(0, import_react5.useEffect)(() => {
|
|
165
|
+
let cancelled = false;
|
|
166
|
+
lr.Fact.findAll(query).then((result) => {
|
|
167
|
+
if (!cancelled) {
|
|
168
|
+
setFacts(result);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return () => {
|
|
172
|
+
cancelled = true;
|
|
173
|
+
};
|
|
174
|
+
}, [lr, queryKey, reloadCount]);
|
|
175
|
+
const reload = (0, import_react5.useCallback)(() => setReloadCount((c) => c + 1), []);
|
|
176
|
+
return { facts, reload };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/useUserInfo.ts
|
|
180
|
+
var import_react6 = require("react");
|
|
181
|
+
function useUserInfo() {
|
|
182
|
+
const { lr } = useLinkedRecords();
|
|
183
|
+
const [userInfo, setUserInfo] = (0, import_react6.useState)(null);
|
|
184
|
+
(0, import_react6.useEffect)(() => {
|
|
159
185
|
lr.getCurrentUserEmail().then((email) => setUserInfo({ email })).catch(() => setUserInfo(null));
|
|
160
186
|
}, [lr]);
|
|
161
187
|
return userInfo;
|
|
162
188
|
}
|
|
189
|
+
|
|
190
|
+
// src/useActorId.ts
|
|
191
|
+
var import_react7 = require("react");
|
|
192
|
+
function useActorId() {
|
|
193
|
+
const { lr } = useLinkedRecords();
|
|
194
|
+
const [actorId, setActorId] = (0, import_react7.useState)(lr.actorId);
|
|
195
|
+
(0, import_react7.useEffect)(() => {
|
|
196
|
+
let cancelled = false;
|
|
197
|
+
lr.ensureUserIdIsKnown().then((id) => {
|
|
198
|
+
if (!cancelled) {
|
|
199
|
+
setActorId(id);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return () => {
|
|
203
|
+
cancelled = true;
|
|
204
|
+
};
|
|
205
|
+
}, [lr]);
|
|
206
|
+
return actorId;
|
|
207
|
+
}
|
|
163
208
|
// Annotate the CommonJS export names for ESM import in node:
|
|
164
209
|
0 && (module.exports = {
|
|
165
210
|
LinkedRecordsContext,
|
|
166
211
|
LinkedRecordsProvider,
|
|
212
|
+
useActorId,
|
|
213
|
+
useFacts,
|
|
167
214
|
useKeyValueAttributes,
|
|
168
215
|
useKeyValueRecords,
|
|
216
|
+
useKeyValueRecordsWithStatus,
|
|
169
217
|
useLinkedRecords,
|
|
170
218
|
useUserInfo
|
|
171
219
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createContext } from "react";
|
|
|
3
3
|
var LinkedRecordsContext = createContext(void 0);
|
|
4
4
|
|
|
5
5
|
// src/LinkedRecordsProvider.tsx
|
|
6
|
-
import { useState } from "react";
|
|
6
|
+
import { useMemo, useState } from "react";
|
|
7
7
|
import LinkedRecords from "@linkedrecords/browser";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
9
9
|
function createClient(serverUrl, config) {
|
|
@@ -22,11 +22,13 @@ function createClient(serverUrl, config) {
|
|
|
22
22
|
}
|
|
23
23
|
function LinkedRecordsProvider({ children, serverUrl, config }) {
|
|
24
24
|
const [lr] = useState(() => config ? createClient(serverUrl, config) : LinkedRecords.getPublicClient(serverUrl));
|
|
25
|
-
|
|
25
|
+
const value = useMemo(() => ({ lr }), [lr]);
|
|
26
|
+
return /* @__PURE__ */ jsx(LinkedRecordsContext.Provider, { value, children });
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// src/useRecords.ts
|
|
29
30
|
import { useEffect, useState as useState2 } from "react";
|
|
31
|
+
import { KeyValueRecord } from "@linkedrecords/browser";
|
|
30
32
|
|
|
31
33
|
// src/useLinkedRecords.ts
|
|
32
34
|
import { useContext } from "react";
|
|
@@ -38,92 +40,135 @@ function useLinkedRecords() {
|
|
|
38
40
|
return context;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
// src/
|
|
42
|
-
import { KeyValueRecord } from "@linkedrecords/browser";
|
|
43
|
+
// src/queryKey.ts
|
|
43
44
|
function getQueryKey(query) {
|
|
44
45
|
return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
|
|
48
|
+
// src/useRecords.ts
|
|
49
|
+
function useKeyValueRecordsWithStatus(query) {
|
|
47
50
|
const { lr } = useLinkedRecords();
|
|
48
51
|
const [records, setRecords] = useState2([]);
|
|
52
|
+
const [isLoading, setIsLoading] = useState2(true);
|
|
49
53
|
const queryKey = getQueryKey(query);
|
|
50
54
|
useEffect(() => {
|
|
55
|
+
setIsLoading(true);
|
|
51
56
|
let cancelled = false;
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const recordUnsubscribes = /* @__PURE__ */ new Map();
|
|
58
|
+
const toKVRecord = async (r) => ({
|
|
59
|
+
_id: r.id,
|
|
60
|
+
_createdAt: r.createdAt,
|
|
61
|
+
...await r.getValue()
|
|
62
|
+
});
|
|
63
|
+
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
64
|
+
records: [
|
|
65
|
+
["$it", "$hasDataType", KeyValueRecord],
|
|
66
|
+
...query
|
|
67
|
+
]
|
|
68
|
+
}, async ({ records: records2 }) => {
|
|
69
|
+
const kvRecords = records2;
|
|
70
|
+
const values = await Promise.all(kvRecords.map(toKVRecord));
|
|
71
|
+
if (cancelled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setRecords(values);
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
const ids = new Set(kvRecords.map((r) => r.id));
|
|
77
|
+
recordUnsubscribes.forEach((unsubscribe, id) => {
|
|
78
|
+
if (!ids.has(id)) {
|
|
79
|
+
recordUnsubscribes.delete(id);
|
|
80
|
+
unsubscribe.then((fn) => fn());
|
|
62
81
|
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
67
|
-
records: [
|
|
68
|
-
["$it", "$hasDataType", KeyValueRecord],
|
|
69
|
-
...query
|
|
70
|
-
]
|
|
71
|
-
}, async ({ records: records2 }) => {
|
|
72
|
-
const values = await Promise.all(records2.map(async (r) => ({
|
|
73
|
-
_id: r.id,
|
|
74
|
-
...await r.getValue()
|
|
75
|
-
})));
|
|
76
|
-
if (cancelled) {
|
|
82
|
+
});
|
|
83
|
+
kvRecords.forEach((r) => {
|
|
84
|
+
if (recordUnsubscribes.has(r.id)) {
|
|
77
85
|
return;
|
|
78
86
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
87
|
+
recordUnsubscribes.set(r.id, r.subscribe(async () => {
|
|
88
|
+
const newValue = await toKVRecord(r);
|
|
89
|
+
if (cancelled) {
|
|
82
90
|
return;
|
|
83
91
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (cancelled) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const newValue = await r.getValue();
|
|
90
|
-
if (cancelled) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
setRecords((prev) => prev.map(
|
|
94
|
-
(v) => v._id === r.id ? { _id: r.id, ...newValue } : v
|
|
95
|
-
));
|
|
96
|
-
});
|
|
97
|
-
});
|
|
92
|
+
setRecords((prev) => prev.map((v) => v._id === r.id ? newValue : v));
|
|
93
|
+
}));
|
|
98
94
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
});
|
|
96
|
+
queryUnsubscribe.catch((err) => {
|
|
97
|
+
console.error("useKeyValueRecords: could not subscribe to query", err);
|
|
102
98
|
});
|
|
103
99
|
return () => {
|
|
104
100
|
cancelled = true;
|
|
105
|
-
|
|
101
|
+
queryUnsubscribe.then((unsubscribe) => unsubscribe(), () => {
|
|
102
|
+
});
|
|
103
|
+
recordUnsubscribes.forEach((unsubscribe) => unsubscribe.then((fn) => fn()));
|
|
106
104
|
};
|
|
107
105
|
}, [lr, queryKey]);
|
|
108
|
-
return records;
|
|
106
|
+
return { records, isLoading };
|
|
107
|
+
}
|
|
108
|
+
function useKeyValueRecords(query) {
|
|
109
|
+
return useKeyValueRecordsWithStatus(query).records;
|
|
109
110
|
}
|
|
110
111
|
var useKeyValueAttributes = useKeyValueRecords;
|
|
111
112
|
|
|
113
|
+
// src/useFacts.ts
|
|
114
|
+
import { useCallback, useEffect as useEffect2, useState as useState3 } from "react";
|
|
115
|
+
function useFacts(query) {
|
|
116
|
+
const { lr } = useLinkedRecords();
|
|
117
|
+
const [facts, setFacts] = useState3([]);
|
|
118
|
+
const [reloadCount, setReloadCount] = useState3(0);
|
|
119
|
+
const queryKey = getQueryKey(query);
|
|
120
|
+
useEffect2(() => {
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
lr.Fact.findAll(query).then((result) => {
|
|
123
|
+
if (!cancelled) {
|
|
124
|
+
setFacts(result);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true;
|
|
129
|
+
};
|
|
130
|
+
}, [lr, queryKey, reloadCount]);
|
|
131
|
+
const reload = useCallback(() => setReloadCount((c) => c + 1), []);
|
|
132
|
+
return { facts, reload };
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
// src/useUserInfo.ts
|
|
113
|
-
import { useEffect as
|
|
136
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
114
137
|
function useUserInfo() {
|
|
115
138
|
const { lr } = useLinkedRecords();
|
|
116
|
-
const [userInfo, setUserInfo] =
|
|
117
|
-
|
|
139
|
+
const [userInfo, setUserInfo] = useState4(null);
|
|
140
|
+
useEffect3(() => {
|
|
118
141
|
lr.getCurrentUserEmail().then((email) => setUserInfo({ email })).catch(() => setUserInfo(null));
|
|
119
142
|
}, [lr]);
|
|
120
143
|
return userInfo;
|
|
121
144
|
}
|
|
145
|
+
|
|
146
|
+
// src/useActorId.ts
|
|
147
|
+
import { useEffect as useEffect4, useState as useState5 } from "react";
|
|
148
|
+
function useActorId() {
|
|
149
|
+
const { lr } = useLinkedRecords();
|
|
150
|
+
const [actorId, setActorId] = useState5(lr.actorId);
|
|
151
|
+
useEffect4(() => {
|
|
152
|
+
let cancelled = false;
|
|
153
|
+
lr.ensureUserIdIsKnown().then((id) => {
|
|
154
|
+
if (!cancelled) {
|
|
155
|
+
setActorId(id);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return () => {
|
|
159
|
+
cancelled = true;
|
|
160
|
+
};
|
|
161
|
+
}, [lr]);
|
|
162
|
+
return actorId;
|
|
163
|
+
}
|
|
122
164
|
export {
|
|
123
165
|
LinkedRecordsContext,
|
|
124
166
|
LinkedRecordsProvider,
|
|
167
|
+
useActorId,
|
|
168
|
+
useFacts,
|
|
125
169
|
useKeyValueAttributes,
|
|
126
170
|
useKeyValueRecords,
|
|
171
|
+
useKeyValueRecordsWithStatus,
|
|
127
172
|
useLinkedRecords,
|
|
128
173
|
useUserInfo
|
|
129
174
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkedrecords/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"description": "",
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@linkedrecords/browser": "^0.
|
|
28
|
+
"@linkedrecords/browser": "^0.4.0",
|
|
29
29
|
"react": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@linkedrecords/browser": "^0.
|
|
32
|
+
"@linkedrecords/browser": "^0.4.0",
|
|
33
33
|
"@types/react": "^19.1.12",
|
|
34
34
|
"react": "^19.1.1",
|
|
35
35
|
"tsup": "^8.5.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
3
3
|
import LinkedRecords from '@linkedrecords/browser';
|
|
4
4
|
import { LinkedRecordsContext } from './linkedRecordsContext';
|
|
5
5
|
|
|
@@ -47,9 +47,11 @@ export function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRec
|
|
|
47
47
|
const [lr] = useState(() => (config
|
|
48
48
|
? createClient(serverUrl, config)
|
|
49
49
|
: LinkedRecords.getPublicClient(serverUrl)));
|
|
50
|
+
// A new context value would re-render every consumer on each render.
|
|
51
|
+
const value = useMemo(() => ({ lr }), [lr]);
|
|
50
52
|
|
|
51
53
|
return (
|
|
52
|
-
<LinkedRecordsContext.Provider value={
|
|
54
|
+
<LinkedRecordsContext.Provider value={value}>
|
|
53
55
|
{children}
|
|
54
56
|
</LinkedRecordsContext.Provider>
|
|
55
57
|
);
|
package/src/index.tsx
CHANGED
package/src/queryKey.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Callers usually pass a new query on every render, so effects depend on a
|
|
2
|
+
// serialized key instead of the query identity. Functions (e.g. record
|
|
3
|
+
// classes) would serialize to null, so they are keyed by name.
|
|
4
|
+
export function getQueryKey(query: unknown): string {
|
|
5
|
+
return JSON.stringify(query, (_, v) => (typeof v === 'function' ? `fn:${v.name}` : v));
|
|
6
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useLinkedRecords } from "./useLinkedRecords";
|
|
3
|
+
|
|
4
|
+
// The id of the logged in user, undefined until it is known.
|
|
5
|
+
export function useActorId(): string | undefined {
|
|
6
|
+
const { lr } = useLinkedRecords();
|
|
7
|
+
const [ actorId, setActorId ] = useState<string | undefined>(lr.actorId);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
|
|
12
|
+
lr.ensureUserIdIsKnown().then((id) => {
|
|
13
|
+
if (!cancelled) {
|
|
14
|
+
setActorId(id);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
cancelled = true;
|
|
20
|
+
}
|
|
21
|
+
}, [ lr ]);
|
|
22
|
+
|
|
23
|
+
return actorId;
|
|
24
|
+
}
|
package/src/useFacts.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type LinkedRecords from '@linkedrecords/browser';
|
|
3
|
+
import { useLinkedRecords } from "./useLinkedRecords";
|
|
4
|
+
import { getQueryKey } from "./queryKey";
|
|
5
|
+
|
|
6
|
+
type FactQuery = Parameters<LinkedRecords['Fact']['findAll']>[0];
|
|
7
|
+
type Fact = Awaited<ReturnType<LinkedRecords['Fact']['findAll']>>[number];
|
|
8
|
+
|
|
9
|
+
// The SDK can not subscribe to facts, so the facts are fetched whenever the
|
|
10
|
+
// query changes. Call reload after writing facts which affect the result.
|
|
11
|
+
export function useFacts(query: FactQuery): { facts: Fact[], reload: () => void } {
|
|
12
|
+
const { lr } = useLinkedRecords();
|
|
13
|
+
const [ facts, setFacts ] = useState<Fact[]>([]);
|
|
14
|
+
const [ reloadCount, setReloadCount ] = useState(0);
|
|
15
|
+
const queryKey = getQueryKey(query);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Ignore responses which arrive after the query changed or the
|
|
19
|
+
// component unmounted.
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
|
|
22
|
+
lr.Fact.findAll(query).then((result) => {
|
|
23
|
+
if (!cancelled) {
|
|
24
|
+
setFacts(result);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
cancelled = true;
|
|
30
|
+
}
|
|
31
|
+
// query is represented by queryKey; depending on the object itself would
|
|
32
|
+
// refetch on every render.
|
|
33
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
+
}, [ lr, queryKey, reloadCount ]);
|
|
35
|
+
|
|
36
|
+
const reload = useCallback(() => setReloadCount((c) => c + 1), []);
|
|
37
|
+
|
|
38
|
+
return { facts, reload };
|
|
39
|
+
}
|
package/src/useRecords.ts
CHANGED
|
@@ -1,106 +1,107 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import { useEffect, useState } from "react";
|
|
3
|
-
import
|
|
2
|
+
import type LinkedRecords from '@linkedrecords/browser';
|
|
4
3
|
import { KeyValueRecord } from '@linkedrecords/browser';
|
|
4
|
+
import { useLinkedRecords } from "./useLinkedRecords";
|
|
5
|
+
import { getQueryKey } from "./queryKey";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
[key: string]: KVValue | string | boolean | number | undefined;
|
|
8
|
-
}
|
|
7
|
+
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
9
|
+
type KVValue = { [key: string]: JSONValue | undefined };
|
|
10
|
+
|
|
11
|
+
export type KVRecord<T extends object = KVValue> = T & {
|
|
12
|
+
_id?: string;
|
|
13
|
+
_createdAt?: Date;
|
|
14
|
+
};
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
type CompoundRecordQuery = Parameters<LinkedRecords['Record']['subscribeToQuery']>[0];
|
|
17
|
+
|
|
18
|
+
// One [subject, predicate, object] clause of a record query, e.g. ['$it', 'isA', 'Todo'].
|
|
19
|
+
export type RecordQueryClause = Exclude<CompoundRecordQuery[string], string>[number];
|
|
20
|
+
|
|
21
|
+
export function useKeyValueRecordsWithStatus<T extends object = KVValue>(query: RecordQueryClause[]): { records: KVRecord<T>[], isLoading: boolean } {
|
|
18
22
|
const { lr } = useLinkedRecords();
|
|
19
|
-
const [ records, setRecords ] = useState<
|
|
23
|
+
const [ records, setRecords ] = useState<KVRecord<T>[]>([]);
|
|
24
|
+
const [ isLoading, setIsLoading ] = useState(true);
|
|
20
25
|
const queryKey = getQueryKey(query);
|
|
21
26
|
|
|
22
27
|
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
// by this effect run check this flag and become no-ops after cleanup.
|
|
25
|
-
let cancelled = false;
|
|
26
|
-
// Records this effect run already observes, so repeated query results
|
|
27
|
-
// do not stack up observers on the same record.
|
|
28
|
-
const subscribedRecordIds = new Set<string | undefined>();
|
|
28
|
+
setIsLoading(true);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// Values are read asynchronously; results which arrive after the query
|
|
31
|
+
// changed or the component unmounted are dropped.
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
// Record observers by record id, so each record in the result is
|
|
34
|
+
// observed once and records which leave the result are unobserved.
|
|
35
|
+
const recordUnsubscribes = new Map<string | undefined, Promise<() => void>>();
|
|
36
|
+
|
|
37
|
+
// T is the value shape declared by the caller; the hook can not check it.
|
|
38
|
+
const toKVRecord = async (r: KeyValueRecord) => ({
|
|
39
|
+
_id: r.id,
|
|
40
|
+
_createdAt: r.createdAt,
|
|
41
|
+
...(await r.getValue()),
|
|
42
|
+
}) as KVRecord<T>;
|
|
43
|
+
|
|
44
|
+
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
45
|
+
records: [
|
|
46
|
+
['$it', '$hasDataType', KeyValueRecord],
|
|
47
|
+
...query
|
|
48
|
+
],
|
|
49
|
+
}, async ({ records }) => {
|
|
50
|
+
const kvRecords = records as KeyValueRecord[];
|
|
51
|
+
const values = await Promise.all(kvRecords.map(toKVRecord));
|
|
52
|
+
|
|
53
|
+
if (cancelled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setRecords(values);
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
|
|
60
|
+
const ids = new Set(kvRecords.map((r) => r.id));
|
|
61
|
+
|
|
62
|
+
recordUnsubscribes.forEach((unsubscribe, id) => {
|
|
63
|
+
if (!ids.has(id)) {
|
|
64
|
+
recordUnsubscribes.delete(id);
|
|
65
|
+
unsubscribe.then((fn) => fn());
|
|
34
66
|
}
|
|
67
|
+
});
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else {
|
|
39
|
-
setTimeout(checkActorId, 50);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
checkActorId();
|
|
43
|
-
}).then(() => {
|
|
44
|
-
const queryUnsubscribe = lr.Record.subscribeToQuery({
|
|
45
|
-
records: [
|
|
46
|
-
['$it', '$hasDataType', KeyValueRecord],
|
|
47
|
-
...query
|
|
48
|
-
],
|
|
49
|
-
}, async ({ records }) => {
|
|
50
|
-
const values = await Promise.all(records.map(async (r) => ({
|
|
51
|
-
_id: r.id,
|
|
52
|
-
...(await r.getValue()),
|
|
53
|
-
})));
|
|
54
|
-
|
|
55
|
-
if (cancelled) {
|
|
69
|
+
kvRecords.forEach((r) => {
|
|
70
|
+
if (recordUnsubscribes.has(r.id)) {
|
|
56
71
|
return;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
recordUnsubscribes.set(r.id, r.subscribe(async () => {
|
|
75
|
+
const newValue = await toKVRecord(r);
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
if (subscribedRecordIds.has(r.id)) {
|
|
77
|
+
if (cancelled) {
|
|
63
78
|
return;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
r.subscribe(async () => {
|
|
69
|
-
if (cancelled) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const newValue = await r.getValue();
|
|
74
|
-
|
|
75
|
-
if (cancelled) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Use functional update to avoid stale closure
|
|
80
|
-
setRecords(prev => prev.map(v =>
|
|
81
|
-
v._id === r.id
|
|
82
|
-
? { _id: r.id, ...newValue }
|
|
83
|
-
: v
|
|
84
|
-
));
|
|
85
|
-
});
|
|
86
|
-
});
|
|
81
|
+
setRecords(prev => prev.map(v => (v._id === r.id ? newValue : v)));
|
|
82
|
+
}));
|
|
87
83
|
});
|
|
84
|
+
});
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
86
|
+
queryUnsubscribe.catch((err) => {
|
|
87
|
+
console.error('useKeyValueRecords: could not subscribe to query', err);
|
|
92
88
|
});
|
|
93
89
|
|
|
94
90
|
return () => {
|
|
95
91
|
cancelled = true;
|
|
96
|
-
|
|
92
|
+
queryUnsubscribe.then((unsubscribe) => unsubscribe(), () => {});
|
|
93
|
+
recordUnsubscribes.forEach((unsubscribe) => unsubscribe.then((fn) => fn()));
|
|
97
94
|
}
|
|
98
95
|
// query is represented by queryKey; depending on the array itself would
|
|
99
96
|
// resubscribe on every render.
|
|
100
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
98
|
}, [ lr, queryKey ]);
|
|
102
99
|
|
|
103
|
-
return records;
|
|
100
|
+
return { records, isLoading };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function useKeyValueRecords<T extends object = KVValue>(query: RecordQueryClause[]): KVRecord<T>[] {
|
|
104
|
+
return useKeyValueRecordsWithStatus<T>(query).records;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/** @deprecated Use useKeyValueRecords instead. */
|