@linkedrecords/react 0.4.0 → 0.6.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 CHANGED
@@ -8,19 +8,46 @@ interface LinkedRecordsContextType {
8
8
  }
9
9
  declare const LinkedRecordsContext: react.Context<LinkedRecordsContextType | undefined>;
10
10
 
11
+ type OIDCConfig = NonNullable<ConstructorParameters<typeof LinkedRecords>[1]>;
12
+ interface LinkedRecordsConfig {
13
+ oidcConfig?: OIDCConfig;
14
+ autoHandleRedirect?: boolean;
15
+ deferUserInfoFetching?: boolean;
16
+ loginHandler?: () => void;
17
+ connectionLostHandler?: (err?: any) => void;
18
+ unknownServerErrorHandler?: (response: any) => void;
19
+ quotaViolationErrorHandler?: (response: any) => void;
20
+ authorizationErrorHandler?: (response: any) => void;
21
+ }
11
22
  interface LinkedRecordsProviderProps {
12
23
  children: ReactNode;
13
24
  serverUrl: string;
25
+ config?: LinkedRecordsConfig;
14
26
  }
15
- declare function LinkedRecordsProvider({ children, serverUrl }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
27
+ declare function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
16
28
 
17
29
  interface KVValue {
18
- [key: string]: KVValue | string | boolean | number | undefined;
30
+ [key: string]: KVValue | string | boolean | number | Date | undefined;
31
+ }
32
+ interface KVRecord extends KVValue {
33
+ _id?: string;
34
+ _createdAt?: Date;
19
35
  }
20
- declare function useKeyValueRecords(query: any[]): KVValue[];
36
+ declare function useKeyValueRecordsWithStatus(query: any[]): {
37
+ records: KVRecord[];
38
+ isLoading: boolean;
39
+ };
40
+ declare function useKeyValueRecords(query: any[]): KVRecord[];
21
41
  /** @deprecated Use useKeyValueRecords instead. */
22
42
  declare const useKeyValueAttributes: typeof useKeyValueRecords;
23
43
 
44
+ type FactQuery = Parameters<LinkedRecords['Fact']['findAll']>[0];
45
+ type Fact = Awaited<ReturnType<LinkedRecords['Fact']['findAll']>>[number];
46
+ declare function useFacts(query: FactQuery): {
47
+ facts: Fact[];
48
+ reload: () => void;
49
+ };
50
+
24
51
  declare function useLinkedRecords(): LinkedRecordsContextType;
25
52
 
26
53
  type UserInfo = {
@@ -28,4 +55,6 @@ type UserInfo = {
28
55
  };
29
56
  declare function useUserInfo(): UserInfo | null;
30
57
 
31
- export { LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
58
+ declare function useActorId(): string | undefined;
59
+
60
+ export { type KVRecord, type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useActorId, useFacts, useKeyValueAttributes, useKeyValueRecords, useKeyValueRecordsWithStatus, useLinkedRecords, useUserInfo };
package/dist/index.d.ts CHANGED
@@ -8,19 +8,46 @@ interface LinkedRecordsContextType {
8
8
  }
9
9
  declare const LinkedRecordsContext: react.Context<LinkedRecordsContextType | undefined>;
10
10
 
11
+ type OIDCConfig = NonNullable<ConstructorParameters<typeof LinkedRecords>[1]>;
12
+ interface LinkedRecordsConfig {
13
+ oidcConfig?: OIDCConfig;
14
+ autoHandleRedirect?: boolean;
15
+ deferUserInfoFetching?: boolean;
16
+ loginHandler?: () => void;
17
+ connectionLostHandler?: (err?: any) => void;
18
+ unknownServerErrorHandler?: (response: any) => void;
19
+ quotaViolationErrorHandler?: (response: any) => void;
20
+ authorizationErrorHandler?: (response: any) => void;
21
+ }
11
22
  interface LinkedRecordsProviderProps {
12
23
  children: ReactNode;
13
24
  serverUrl: string;
25
+ config?: LinkedRecordsConfig;
14
26
  }
15
- declare function LinkedRecordsProvider({ children, serverUrl }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
27
+ declare function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRecordsProviderProps): react_jsx_runtime.JSX.Element;
16
28
 
17
29
  interface KVValue {
18
- [key: string]: KVValue | string | boolean | number | undefined;
30
+ [key: string]: KVValue | string | boolean | number | Date | undefined;
31
+ }
32
+ interface KVRecord extends KVValue {
33
+ _id?: string;
34
+ _createdAt?: Date;
19
35
  }
20
- declare function useKeyValueRecords(query: any[]): KVValue[];
36
+ declare function useKeyValueRecordsWithStatus(query: any[]): {
37
+ records: KVRecord[];
38
+ isLoading: boolean;
39
+ };
40
+ declare function useKeyValueRecords(query: any[]): KVRecord[];
21
41
  /** @deprecated Use useKeyValueRecords instead. */
22
42
  declare const useKeyValueAttributes: typeof useKeyValueRecords;
23
43
 
44
+ type FactQuery = Parameters<LinkedRecords['Fact']['findAll']>[0];
45
+ type Fact = Awaited<ReturnType<LinkedRecords['Fact']['findAll']>>[number];
46
+ declare function useFacts(query: FactQuery): {
47
+ facts: Fact[];
48
+ reload: () => void;
49
+ };
50
+
24
51
  declare function useLinkedRecords(): LinkedRecordsContextType;
25
52
 
26
53
  type UserInfo = {
@@ -28,4 +55,6 @@ type UserInfo = {
28
55
  };
29
56
  declare function useUserInfo(): UserInfo | null;
30
57
 
31
- export { LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
58
+ declare function useActorId(): string | undefined;
59
+
60
+ export { type KVRecord, type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, 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
  });
@@ -47,8 +50,22 @@ var LinkedRecordsContext = (0, import_react.createContext)(void 0);
47
50
  var import_react2 = require("react");
48
51
  var import_browser = __toESM(require("@linkedrecords/browser"));
49
52
  var import_jsx_runtime = require("react/jsx-runtime");
50
- function LinkedRecordsProvider({ children, serverUrl }) {
51
- const lr = import_browser.default.getPublicClient(serverUrl);
53
+ function createClient(serverUrl, config) {
54
+ const lr = new import_browser.default(
55
+ new URL(serverUrl),
56
+ config.oidcConfig,
57
+ config.autoHandleRedirect,
58
+ config.deferUserInfoFetching
59
+ );
60
+ if (config.loginHandler) lr.setLoginHandler(config.loginHandler);
61
+ if (config.connectionLostHandler) lr.setConnectionLostHandler(config.connectionLostHandler);
62
+ if (config.unknownServerErrorHandler) lr.setUnknownServerErrorHandler(config.unknownServerErrorHandler);
63
+ if (config.quotaViolationErrorHandler) lr.setQuotaViolationErrorHandler(config.quotaViolationErrorHandler);
64
+ if (config.authorizationErrorHandler) lr.setAuthorizationErrorHandler(config.authorizationErrorHandler);
65
+ return lr;
66
+ }
67
+ function LinkedRecordsProvider({ children, serverUrl, config }) {
68
+ const [lr] = (0, import_react2.useState)(() => config ? createClient(serverUrl, config) : import_browser.default.getPublicClient(serverUrl));
52
69
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedRecordsContext.Provider, { value: { lr }, children });
53
70
  }
54
71
 
@@ -65,14 +82,27 @@ function useLinkedRecords() {
65
82
  return context;
66
83
  }
67
84
 
85
+ // src/queryKey.ts
86
+ function getQueryKey(query) {
87
+ return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
88
+ }
89
+
68
90
  // src/useRecords.ts
69
91
  var import_browser2 = require("@linkedrecords/browser");
70
- function useKeyValueRecords(query) {
92
+ function useKeyValueRecordsWithStatus(query) {
71
93
  const { lr } = useLinkedRecords();
72
94
  const [records, setRecords] = (0, import_react4.useState)([]);
95
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(true);
96
+ const queryKey = getQueryKey(query);
73
97
  (0, import_react4.useEffect)(() => {
98
+ setIsLoading(true);
99
+ let cancelled = false;
100
+ const subscribedRecordIds = /* @__PURE__ */ new Set();
74
101
  const unsubscribeFnPromise = new Promise((resolve) => {
75
102
  const checkActorId = () => {
103
+ if (cancelled) {
104
+ return;
105
+ }
76
106
  if (lr.actorId !== void 0) {
77
107
  resolve();
78
108
  } else {
@@ -89,14 +119,29 @@ function useKeyValueRecords(query) {
89
119
  }, async ({ records: records2 }) => {
90
120
  const values = await Promise.all(records2.map(async (r) => ({
91
121
  _id: r.id,
122
+ _createdAt: r.createdAt,
92
123
  ...await r.getValue()
93
124
  })));
125
+ if (cancelled) {
126
+ return;
127
+ }
94
128
  setRecords(values);
129
+ setIsLoading(false);
95
130
  records2.forEach((r) => {
131
+ if (subscribedRecordIds.has(r.id)) {
132
+ return;
133
+ }
134
+ subscribedRecordIds.add(r.id);
96
135
  r.subscribe(async () => {
136
+ if (cancelled) {
137
+ return;
138
+ }
97
139
  const newValue = await r.getValue();
140
+ if (cancelled) {
141
+ return;
142
+ }
98
143
  setRecords((prev) => prev.map(
99
- (v) => v._id === r.id ? { _id: r.id, ...newValue } : v
144
+ (v) => v._id === r.id ? { _id: r.id, _createdAt: r.createdAt, ...newValue } : v
100
145
  ));
101
146
  });
102
147
  });
@@ -106,29 +151,77 @@ function useKeyValueRecords(query) {
106
151
  };
107
152
  });
108
153
  return () => {
154
+ cancelled = true;
109
155
  unsubscribeFnPromise.then((fn) => fn());
110
156
  };
111
- }, [lr.Record, setRecords]);
112
- return records;
157
+ }, [lr, queryKey]);
158
+ return { records, isLoading };
159
+ }
160
+ function useKeyValueRecords(query) {
161
+ return useKeyValueRecordsWithStatus(query).records;
113
162
  }
114
163
  var useKeyValueAttributes = useKeyValueRecords;
115
164
 
116
- // src/useUserInfo.ts
165
+ // src/useFacts.ts
117
166
  var import_react5 = require("react");
118
- function useUserInfo() {
167
+ function useFacts(query) {
119
168
  const { lr } = useLinkedRecords();
120
- const [userInfo, setUserInfo] = (0, import_react5.useState)(null);
169
+ const [facts, setFacts] = (0, import_react5.useState)([]);
170
+ const [reloadCount, setReloadCount] = (0, import_react5.useState)(0);
171
+ const queryKey = getQueryKey(query);
121
172
  (0, import_react5.useEffect)(() => {
173
+ let cancelled = false;
174
+ lr.Fact.findAll(query).then((result) => {
175
+ if (!cancelled) {
176
+ setFacts(result);
177
+ }
178
+ });
179
+ return () => {
180
+ cancelled = true;
181
+ };
182
+ }, [lr, queryKey, reloadCount]);
183
+ const reload = (0, import_react5.useCallback)(() => setReloadCount((c) => c + 1), []);
184
+ return { facts, reload };
185
+ }
186
+
187
+ // src/useUserInfo.ts
188
+ var import_react6 = require("react");
189
+ function useUserInfo() {
190
+ const { lr } = useLinkedRecords();
191
+ const [userInfo, setUserInfo] = (0, import_react6.useState)(null);
192
+ (0, import_react6.useEffect)(() => {
122
193
  lr.getCurrentUserEmail().then((email) => setUserInfo({ email })).catch(() => setUserInfo(null));
123
194
  }, [lr]);
124
195
  return userInfo;
125
196
  }
197
+
198
+ // src/useActorId.ts
199
+ var import_react7 = require("react");
200
+ function useActorId() {
201
+ const { lr } = useLinkedRecords();
202
+ const [actorId, setActorId] = (0, import_react7.useState)(lr.actorId);
203
+ (0, import_react7.useEffect)(() => {
204
+ let cancelled = false;
205
+ lr.ensureUserIdIsKnown().then((id) => {
206
+ if (!cancelled) {
207
+ setActorId(id);
208
+ }
209
+ });
210
+ return () => {
211
+ cancelled = true;
212
+ };
213
+ }, [lr]);
214
+ return actorId;
215
+ }
126
216
  // Annotate the CommonJS export names for ESM import in node:
127
217
  0 && (module.exports = {
128
218
  LinkedRecordsContext,
129
219
  LinkedRecordsProvider,
220
+ useActorId,
221
+ useFacts,
130
222
  useKeyValueAttributes,
131
223
  useKeyValueRecords,
224
+ useKeyValueRecordsWithStatus,
132
225
  useLinkedRecords,
133
226
  useUserInfo
134
227
  });
package/dist/index.mjs CHANGED
@@ -3,16 +3,30 @@ import { createContext } from "react";
3
3
  var LinkedRecordsContext = createContext(void 0);
4
4
 
5
5
  // src/LinkedRecordsProvider.tsx
6
- import "react";
6
+ import { useState } from "react";
7
7
  import LinkedRecords from "@linkedrecords/browser";
8
8
  import { jsx } from "react/jsx-runtime";
9
- function LinkedRecordsProvider({ children, serverUrl }) {
10
- const lr = LinkedRecords.getPublicClient(serverUrl);
9
+ function createClient(serverUrl, config) {
10
+ const lr = new LinkedRecords(
11
+ new URL(serverUrl),
12
+ config.oidcConfig,
13
+ config.autoHandleRedirect,
14
+ config.deferUserInfoFetching
15
+ );
16
+ if (config.loginHandler) lr.setLoginHandler(config.loginHandler);
17
+ if (config.connectionLostHandler) lr.setConnectionLostHandler(config.connectionLostHandler);
18
+ if (config.unknownServerErrorHandler) lr.setUnknownServerErrorHandler(config.unknownServerErrorHandler);
19
+ if (config.quotaViolationErrorHandler) lr.setQuotaViolationErrorHandler(config.quotaViolationErrorHandler);
20
+ if (config.authorizationErrorHandler) lr.setAuthorizationErrorHandler(config.authorizationErrorHandler);
21
+ return lr;
22
+ }
23
+ function LinkedRecordsProvider({ children, serverUrl, config }) {
24
+ const [lr] = useState(() => config ? createClient(serverUrl, config) : LinkedRecords.getPublicClient(serverUrl));
11
25
  return /* @__PURE__ */ jsx(LinkedRecordsContext.Provider, { value: { lr }, children });
12
26
  }
13
27
 
14
28
  // src/useRecords.ts
15
- import { useEffect, useState } from "react";
29
+ import { useEffect, useState as useState2 } from "react";
16
30
 
17
31
  // src/useLinkedRecords.ts
18
32
  import { useContext } from "react";
@@ -24,14 +38,27 @@ function useLinkedRecords() {
24
38
  return context;
25
39
  }
26
40
 
41
+ // src/queryKey.ts
42
+ function getQueryKey(query) {
43
+ return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
44
+ }
45
+
27
46
  // src/useRecords.ts
28
47
  import { KeyValueRecord } from "@linkedrecords/browser";
29
- function useKeyValueRecords(query) {
48
+ function useKeyValueRecordsWithStatus(query) {
30
49
  const { lr } = useLinkedRecords();
31
- const [records, setRecords] = useState([]);
50
+ const [records, setRecords] = useState2([]);
51
+ const [isLoading, setIsLoading] = useState2(true);
52
+ const queryKey = getQueryKey(query);
32
53
  useEffect(() => {
54
+ setIsLoading(true);
55
+ let cancelled = false;
56
+ const subscribedRecordIds = /* @__PURE__ */ new Set();
33
57
  const unsubscribeFnPromise = new Promise((resolve) => {
34
58
  const checkActorId = () => {
59
+ if (cancelled) {
60
+ return;
61
+ }
35
62
  if (lr.actorId !== void 0) {
36
63
  resolve();
37
64
  } else {
@@ -48,14 +75,29 @@ function useKeyValueRecords(query) {
48
75
  }, async ({ records: records2 }) => {
49
76
  const values = await Promise.all(records2.map(async (r) => ({
50
77
  _id: r.id,
78
+ _createdAt: r.createdAt,
51
79
  ...await r.getValue()
52
80
  })));
81
+ if (cancelled) {
82
+ return;
83
+ }
53
84
  setRecords(values);
85
+ setIsLoading(false);
54
86
  records2.forEach((r) => {
87
+ if (subscribedRecordIds.has(r.id)) {
88
+ return;
89
+ }
90
+ subscribedRecordIds.add(r.id);
55
91
  r.subscribe(async () => {
92
+ if (cancelled) {
93
+ return;
94
+ }
56
95
  const newValue = await r.getValue();
96
+ if (cancelled) {
97
+ return;
98
+ }
57
99
  setRecords((prev) => prev.map(
58
- (v) => v._id === r.id ? { _id: r.id, ...newValue } : v
100
+ (v) => v._id === r.id ? { _id: r.id, _createdAt: r.createdAt, ...newValue } : v
59
101
  ));
60
102
  });
61
103
  });
@@ -65,28 +107,76 @@ function useKeyValueRecords(query) {
65
107
  };
66
108
  });
67
109
  return () => {
110
+ cancelled = true;
68
111
  unsubscribeFnPromise.then((fn) => fn());
69
112
  };
70
- }, [lr.Record, setRecords]);
71
- return records;
113
+ }, [lr, queryKey]);
114
+ return { records, isLoading };
115
+ }
116
+ function useKeyValueRecords(query) {
117
+ return useKeyValueRecordsWithStatus(query).records;
72
118
  }
73
119
  var useKeyValueAttributes = useKeyValueRecords;
74
120
 
121
+ // src/useFacts.ts
122
+ import { useCallback, useEffect as useEffect2, useState as useState3 } from "react";
123
+ function useFacts(query) {
124
+ const { lr } = useLinkedRecords();
125
+ const [facts, setFacts] = useState3([]);
126
+ const [reloadCount, setReloadCount] = useState3(0);
127
+ const queryKey = getQueryKey(query);
128
+ useEffect2(() => {
129
+ let cancelled = false;
130
+ lr.Fact.findAll(query).then((result) => {
131
+ if (!cancelled) {
132
+ setFacts(result);
133
+ }
134
+ });
135
+ return () => {
136
+ cancelled = true;
137
+ };
138
+ }, [lr, queryKey, reloadCount]);
139
+ const reload = useCallback(() => setReloadCount((c) => c + 1), []);
140
+ return { facts, reload };
141
+ }
142
+
75
143
  // src/useUserInfo.ts
76
- import { useEffect as useEffect2, useState as useState2 } from "react";
144
+ import { useEffect as useEffect3, useState as useState4 } from "react";
77
145
  function useUserInfo() {
78
146
  const { lr } = useLinkedRecords();
79
- const [userInfo, setUserInfo] = useState2(null);
80
- useEffect2(() => {
147
+ const [userInfo, setUserInfo] = useState4(null);
148
+ useEffect3(() => {
81
149
  lr.getCurrentUserEmail().then((email) => setUserInfo({ email })).catch(() => setUserInfo(null));
82
150
  }, [lr]);
83
151
  return userInfo;
84
152
  }
153
+
154
+ // src/useActorId.ts
155
+ import { useEffect as useEffect4, useState as useState5 } from "react";
156
+ function useActorId() {
157
+ const { lr } = useLinkedRecords();
158
+ const [actorId, setActorId] = useState5(lr.actorId);
159
+ useEffect4(() => {
160
+ let cancelled = false;
161
+ lr.ensureUserIdIsKnown().then((id) => {
162
+ if (!cancelled) {
163
+ setActorId(id);
164
+ }
165
+ });
166
+ return () => {
167
+ cancelled = true;
168
+ };
169
+ }, [lr]);
170
+ return actorId;
171
+ }
85
172
  export {
86
173
  LinkedRecordsContext,
87
174
  LinkedRecordsProvider,
175
+ useActorId,
176
+ useFacts,
88
177
  useKeyValueAttributes,
89
178
  useKeyValueRecords,
179
+ useKeyValueRecordsWithStatus,
90
180
  useLinkedRecords,
91
181
  useUserInfo
92
182
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkedrecords/react",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -1,14 +1,52 @@
1
- import { type ReactNode } from 'react';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { type ReactNode, useState } from 'react';
2
3
  import LinkedRecords from '@linkedrecords/browser';
3
4
  import { LinkedRecordsContext } from './linkedRecordsContext';
4
5
 
6
+ type OIDCConfig = NonNullable<ConstructorParameters<typeof LinkedRecords>[1]>;
7
+
8
+ export interface LinkedRecordsConfig {
9
+ // Omit to use cookie based auth, where the LinkedRecords server handles the login.
10
+ oidcConfig?: OIDCConfig;
11
+ autoHandleRedirect?: boolean;
12
+ deferUserInfoFetching?: boolean;
13
+ loginHandler?: () => void;
14
+ connectionLostHandler?: (err?: any) => void;
15
+ unknownServerErrorHandler?: (response: any) => void;
16
+ quotaViolationErrorHandler?: (response: any) => void;
17
+ authorizationErrorHandler?: (response: any) => void;
18
+ }
19
+
5
20
  interface LinkedRecordsProviderProps {
6
21
  children: ReactNode;
7
22
  serverUrl: string;
23
+ // Read once on mount. Without it, the shared public client for serverUrl is used.
24
+ config?: LinkedRecordsConfig;
25
+ }
26
+
27
+ function createClient(serverUrl: string, config: LinkedRecordsConfig): LinkedRecords {
28
+ const lr = new LinkedRecords(
29
+ new URL(serverUrl),
30
+ config.oidcConfig,
31
+ config.autoHandleRedirect,
32
+ config.deferUserInfoFetching,
33
+ );
34
+
35
+ // Handlers are installed before any child renders, so the first
36
+ // requests made by child components already go through them.
37
+ if (config.loginHandler) lr.setLoginHandler(config.loginHandler);
38
+ if (config.connectionLostHandler) lr.setConnectionLostHandler(config.connectionLostHandler);
39
+ if (config.unknownServerErrorHandler) lr.setUnknownServerErrorHandler(config.unknownServerErrorHandler);
40
+ if (config.quotaViolationErrorHandler) lr.setQuotaViolationErrorHandler(config.quotaViolationErrorHandler);
41
+ if (config.authorizationErrorHandler) lr.setAuthorizationErrorHandler(config.authorizationErrorHandler);
42
+
43
+ return lr;
8
44
  }
9
45
 
10
- export function LinkedRecordsProvider({ children, serverUrl }: LinkedRecordsProviderProps) {
11
- const lr = LinkedRecords.getPublicClient(serverUrl);
46
+ export function LinkedRecordsProvider({ children, serverUrl, config }: LinkedRecordsProviderProps) {
47
+ const [lr] = useState(() => (config
48
+ ? createClient(serverUrl, config)
49
+ : LinkedRecords.getPublicClient(serverUrl)));
12
50
 
13
51
  return (
14
52
  <LinkedRecordsContext.Provider value={{ lr }}>
package/src/index.tsx CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './linkedRecordsContext';
2
2
  export * from './LinkedRecordsProvider';
3
3
  export * from './useRecords';
4
+ export * from './useFacts';
4
5
  export * from './useLinkedRecords';
5
6
  export * from './useUserInfo';
7
+ export * from './useActorId';
@@ -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
+ }
@@ -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,19 +1,40 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { useEffect, useState } from "react";
3
3
  import { useLinkedRecords } from "./useLinkedRecords";
4
+ import { getQueryKey } from "./queryKey";
4
5
  import { KeyValueRecord } from '@linkedrecords/browser';
5
6
 
6
7
  interface KVValue {
7
- [key: string]: KVValue | string | boolean | number | undefined;
8
+ [key: string]: KVValue | string | boolean | number | Date | undefined;
8
9
  }
9
10
 
10
- export function useKeyValueRecords(query: any[]): KVValue[] {
11
+ export interface KVRecord extends KVValue {
12
+ _id?: string;
13
+ _createdAt?: Date;
14
+ }
15
+
16
+ export function useKeyValueRecordsWithStatus(query: any[]): { records: KVRecord[], isLoading: boolean } {
11
17
  const { lr } = useLinkedRecords();
12
- const [ records, setRecords ] = useState<KVValue[]>([]);
18
+ const [ records, setRecords ] = useState<KVRecord[]>([]);
19
+ const [ isLoading, setIsLoading ] = useState(true);
20
+ const queryKey = getQueryKey(query);
13
21
 
14
22
  useEffect(() => {
23
+ setIsLoading(true);
24
+
25
+ // The SDK has no way to remove a record observer, so observers registered
26
+ // by this effect run check this flag and become no-ops after cleanup.
27
+ let cancelled = false;
28
+ // Records this effect run already observes, so repeated query results
29
+ // do not stack up observers on the same record.
30
+ const subscribedRecordIds = new Set<string | undefined>();
31
+
15
32
  const unsubscribeFnPromise = new Promise<void>((resolve) => {
16
33
  const checkActorId = () => {
34
+ if (cancelled) {
35
+ return;
36
+ }
37
+
17
38
  if (lr.actorId !== undefined) {
18
39
  resolve();
19
40
  } else {
@@ -30,19 +51,39 @@ export function useKeyValueRecords(query: any[]): KVValue[] {
30
51
  }, async ({ records }) => {
31
52
  const values = await Promise.all(records.map(async (r) => ({
32
53
  _id: r.id,
54
+ _createdAt: r.createdAt,
33
55
  ...(await r.getValue()),
34
56
  })));
35
57
 
58
+ if (cancelled) {
59
+ return;
60
+ }
61
+
36
62
  setRecords(values);
63
+ setIsLoading(false);
37
64
 
38
65
  records.forEach((r) => {
66
+ if (subscribedRecordIds.has(r.id)) {
67
+ return;
68
+ }
69
+
70
+ subscribedRecordIds.add(r.id);
71
+
39
72
  r.subscribe(async () => {
73
+ if (cancelled) {
74
+ return;
75
+ }
76
+
40
77
  const newValue = await r.getValue();
41
78
 
79
+ if (cancelled) {
80
+ return;
81
+ }
82
+
42
83
  // Use functional update to avoid stale closure
43
84
  setRecords(prev => prev.map(v =>
44
85
  v._id === r.id
45
- ? { _id: r.id, ...newValue }
86
+ ? { _id: r.id, _createdAt: r.createdAt, ...newValue }
46
87
  : v
47
88
  ));
48
89
  });
@@ -55,11 +96,19 @@ export function useKeyValueRecords(query: any[]): KVValue[] {
55
96
  });
56
97
 
57
98
  return () => {
99
+ cancelled = true;
58
100
  unsubscribeFnPromise.then(fn => fn());
59
101
  }
60
- }, [ lr.Record, setRecords ]);
102
+ // query is represented by queryKey; depending on the array itself would
103
+ // resubscribe on every render.
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ }, [ lr, queryKey ]);
106
+
107
+ return { records, isLoading };
108
+ }
61
109
 
62
- return records;
110
+ export function useKeyValueRecords(query: any[]): KVRecord[] {
111
+ return useKeyValueRecordsWithStatus(query).records;
63
112
  }
64
113
 
65
114
  /** @deprecated Use useKeyValueRecords instead. */