@linkedrecords/react 0.4.0 → 0.5.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,11 +8,23 @@ 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
30
  [key: string]: KVValue | string | boolean | number | undefined;
@@ -28,4 +40,4 @@ type UserInfo = {
28
40
  };
29
41
  declare function useUserInfo(): UserInfo | null;
30
42
 
31
- export { LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
43
+ export { type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
package/dist/index.d.ts CHANGED
@@ -8,11 +8,23 @@ 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
30
  [key: string]: KVValue | string | boolean | number | undefined;
@@ -28,4 +40,4 @@ type UserInfo = {
28
40
  };
29
41
  declare function useUserInfo(): UserInfo | null;
30
42
 
31
- export { LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
43
+ export { type LinkedRecordsConfig, LinkedRecordsContext, type LinkedRecordsContextType, LinkedRecordsProvider, useKeyValueAttributes, useKeyValueRecords, useLinkedRecords, useUserInfo };
package/dist/index.js CHANGED
@@ -47,8 +47,22 @@ var LinkedRecordsContext = (0, import_react.createContext)(void 0);
47
47
  var import_react2 = require("react");
48
48
  var import_browser = __toESM(require("@linkedrecords/browser"));
49
49
  var import_jsx_runtime = require("react/jsx-runtime");
50
- function LinkedRecordsProvider({ children, serverUrl }) {
51
- const lr = import_browser.default.getPublicClient(serverUrl);
50
+ function createClient(serverUrl, config) {
51
+ const lr = new import_browser.default(
52
+ new URL(serverUrl),
53
+ config.oidcConfig,
54
+ config.autoHandleRedirect,
55
+ config.deferUserInfoFetching
56
+ );
57
+ if (config.loginHandler) lr.setLoginHandler(config.loginHandler);
58
+ if (config.connectionLostHandler) lr.setConnectionLostHandler(config.connectionLostHandler);
59
+ if (config.unknownServerErrorHandler) lr.setUnknownServerErrorHandler(config.unknownServerErrorHandler);
60
+ if (config.quotaViolationErrorHandler) lr.setQuotaViolationErrorHandler(config.quotaViolationErrorHandler);
61
+ if (config.authorizationErrorHandler) lr.setAuthorizationErrorHandler(config.authorizationErrorHandler);
62
+ return lr;
63
+ }
64
+ function LinkedRecordsProvider({ children, serverUrl, config }) {
65
+ const [lr] = (0, import_react2.useState)(() => config ? createClient(serverUrl, config) : import_browser.default.getPublicClient(serverUrl));
52
66
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedRecordsContext.Provider, { value: { lr }, children });
53
67
  }
54
68
 
@@ -67,12 +81,21 @@ function useLinkedRecords() {
67
81
 
68
82
  // src/useRecords.ts
69
83
  var import_browser2 = require("@linkedrecords/browser");
84
+ function getQueryKey(query) {
85
+ return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
86
+ }
70
87
  function useKeyValueRecords(query) {
71
88
  const { lr } = useLinkedRecords();
72
89
  const [records, setRecords] = (0, import_react4.useState)([]);
90
+ const queryKey = getQueryKey(query);
73
91
  (0, import_react4.useEffect)(() => {
92
+ let cancelled = false;
93
+ const subscribedRecordIds = /* @__PURE__ */ new Set();
74
94
  const unsubscribeFnPromise = new Promise((resolve) => {
75
95
  const checkActorId = () => {
96
+ if (cancelled) {
97
+ return;
98
+ }
76
99
  if (lr.actorId !== void 0) {
77
100
  resolve();
78
101
  } else {
@@ -91,10 +114,23 @@ function useKeyValueRecords(query) {
91
114
  _id: r.id,
92
115
  ...await r.getValue()
93
116
  })));
117
+ if (cancelled) {
118
+ return;
119
+ }
94
120
  setRecords(values);
95
121
  records2.forEach((r) => {
122
+ if (subscribedRecordIds.has(r.id)) {
123
+ return;
124
+ }
125
+ subscribedRecordIds.add(r.id);
96
126
  r.subscribe(async () => {
127
+ if (cancelled) {
128
+ return;
129
+ }
97
130
  const newValue = await r.getValue();
131
+ if (cancelled) {
132
+ return;
133
+ }
98
134
  setRecords((prev) => prev.map(
99
135
  (v) => v._id === r.id ? { _id: r.id, ...newValue } : v
100
136
  ));
@@ -106,9 +142,10 @@ function useKeyValueRecords(query) {
106
142
  };
107
143
  });
108
144
  return () => {
145
+ cancelled = true;
109
146
  unsubscribeFnPromise.then((fn) => fn());
110
147
  };
111
- }, [lr.Record, setRecords]);
148
+ }, [lr, queryKey]);
112
149
  return records;
113
150
  }
114
151
  var useKeyValueAttributes = useKeyValueRecords;
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";
@@ -26,12 +40,21 @@ function useLinkedRecords() {
26
40
 
27
41
  // src/useRecords.ts
28
42
  import { KeyValueRecord } from "@linkedrecords/browser";
43
+ function getQueryKey(query) {
44
+ return JSON.stringify(query, (_, v) => typeof v === "function" ? `fn:${v.name}` : v);
45
+ }
29
46
  function useKeyValueRecords(query) {
30
47
  const { lr } = useLinkedRecords();
31
- const [records, setRecords] = useState([]);
48
+ const [records, setRecords] = useState2([]);
49
+ const queryKey = getQueryKey(query);
32
50
  useEffect(() => {
51
+ let cancelled = false;
52
+ const subscribedRecordIds = /* @__PURE__ */ new Set();
33
53
  const unsubscribeFnPromise = new Promise((resolve) => {
34
54
  const checkActorId = () => {
55
+ if (cancelled) {
56
+ return;
57
+ }
35
58
  if (lr.actorId !== void 0) {
36
59
  resolve();
37
60
  } else {
@@ -50,10 +73,23 @@ function useKeyValueRecords(query) {
50
73
  _id: r.id,
51
74
  ...await r.getValue()
52
75
  })));
76
+ if (cancelled) {
77
+ return;
78
+ }
53
79
  setRecords(values);
54
80
  records2.forEach((r) => {
81
+ if (subscribedRecordIds.has(r.id)) {
82
+ return;
83
+ }
84
+ subscribedRecordIds.add(r.id);
55
85
  r.subscribe(async () => {
86
+ if (cancelled) {
87
+ return;
88
+ }
56
89
  const newValue = await r.getValue();
90
+ if (cancelled) {
91
+ return;
92
+ }
57
93
  setRecords((prev) => prev.map(
58
94
  (v) => v._id === r.id ? { _id: r.id, ...newValue } : v
59
95
  ));
@@ -65,18 +101,19 @@ function useKeyValueRecords(query) {
65
101
  };
66
102
  });
67
103
  return () => {
104
+ cancelled = true;
68
105
  unsubscribeFnPromise.then((fn) => fn());
69
106
  };
70
- }, [lr.Record, setRecords]);
107
+ }, [lr, queryKey]);
71
108
  return records;
72
109
  }
73
110
  var useKeyValueAttributes = useKeyValueRecords;
74
111
 
75
112
  // src/useUserInfo.ts
76
- import { useEffect as useEffect2, useState as useState2 } from "react";
113
+ import { useEffect as useEffect2, useState as useState3 } from "react";
77
114
  function useUserInfo() {
78
115
  const { lr } = useLinkedRecords();
79
- const [userInfo, setUserInfo] = useState2(null);
116
+ const [userInfo, setUserInfo] = useState3(null);
80
117
  useEffect2(() => {
81
118
  lr.getCurrentUserEmail().then((email) => setUserInfo({ email })).catch(() => setUserInfo(null));
82
119
  }, [lr]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkedrecords/react",
3
- "version": "0.4.0",
3
+ "version": "0.5.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/useRecords.ts CHANGED
@@ -7,13 +7,32 @@ interface KVValue {
7
7
  [key: string]: KVValue | string | boolean | number | undefined;
8
8
  }
9
9
 
10
+ // Callers usually pass a new query array on every render, so the effect
11
+ // depends on a serialized key instead of the array identity. Functions
12
+ // (e.g. record classes) would serialize to null, so they are keyed by name.
13
+ function getQueryKey(query: any[]): string {
14
+ return JSON.stringify(query, (_, v) => (typeof v === 'function' ? `fn:${v.name}` : v));
15
+ }
16
+
10
17
  export function useKeyValueRecords(query: any[]): KVValue[] {
11
18
  const { lr } = useLinkedRecords();
12
19
  const [ records, setRecords ] = useState<KVValue[]>([]);
20
+ const queryKey = getQueryKey(query);
13
21
 
14
22
  useEffect(() => {
23
+ // The SDK has no way to remove a record observer, so observers registered
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>();
29
+
15
30
  const unsubscribeFnPromise = new Promise<void>((resolve) => {
16
31
  const checkActorId = () => {
32
+ if (cancelled) {
33
+ return;
34
+ }
35
+
17
36
  if (lr.actorId !== undefined) {
18
37
  resolve();
19
38
  } else {
@@ -33,12 +52,30 @@ export function useKeyValueRecords(query: any[]): KVValue[] {
33
52
  ...(await r.getValue()),
34
53
  })));
35
54
 
55
+ if (cancelled) {
56
+ return;
57
+ }
58
+
36
59
  setRecords(values);
37
60
 
38
61
  records.forEach((r) => {
62
+ if (subscribedRecordIds.has(r.id)) {
63
+ return;
64
+ }
65
+
66
+ subscribedRecordIds.add(r.id);
67
+
39
68
  r.subscribe(async () => {
69
+ if (cancelled) {
70
+ return;
71
+ }
72
+
40
73
  const newValue = await r.getValue();
41
74
 
75
+ if (cancelled) {
76
+ return;
77
+ }
78
+
42
79
  // Use functional update to avoid stale closure
43
80
  setRecords(prev => prev.map(v =>
44
81
  v._id === r.id
@@ -55,9 +92,13 @@ export function useKeyValueRecords(query: any[]): KVValue[] {
55
92
  });
56
93
 
57
94
  return () => {
95
+ cancelled = true;
58
96
  unsubscribeFnPromise.then(fn => fn());
59
97
  }
60
- }, [ lr.Record, setRecords ]);
98
+ // query is represented by queryKey; depending on the array itself would
99
+ // resubscribe on every render.
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, [ lr, queryKey ]);
61
102
 
62
103
  return records;
63
104
  }