jazz-tools 0.18.6 → 0.18.8

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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +55 -55
  2. package/CHANGELOG.md +28 -0
  3. package/dist/better-auth/auth/server.d.ts.map +1 -1
  4. package/dist/better-auth/auth/server.js +8 -4
  5. package/dist/better-auth/auth/server.js.map +1 -1
  6. package/dist/{chunk-45VKEOXG.js → chunk-QF3R3C4N.js} +75 -22
  7. package/dist/chunk-QF3R3C4N.js.map +1 -0
  8. package/dist/index.js +1 -1
  9. package/dist/inspector/{custom-element-IBHKHN27.js → custom-element-G6SPZEBR.js} +292 -31
  10. package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
  11. package/dist/inspector/index.d.ts +1 -1
  12. package/dist/inspector/index.js +302 -41
  13. package/dist/inspector/index.js.map +1 -1
  14. package/dist/inspector/register-custom-element.js +1 -1
  15. package/dist/inspector/ui/button.d.ts +1 -1
  16. package/dist/inspector/ui/button.d.ts.map +1 -1
  17. package/dist/inspector/ui/heading.d.ts +2 -1
  18. package/dist/inspector/ui/heading.d.ts.map +1 -1
  19. package/dist/inspector/ui/input.d.ts.map +1 -1
  20. package/dist/inspector/ui/modal.d.ts +16 -0
  21. package/dist/inspector/ui/modal.d.ts.map +1 -0
  22. package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
  23. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
  24. package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
  25. package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
  26. package/dist/inspector/viewer/new-app.d.ts +1 -1
  27. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  28. package/dist/react/hooks.d.ts +1 -1
  29. package/dist/react/hooks.d.ts.map +1 -1
  30. package/dist/react/index.d.ts +1 -1
  31. package/dist/react/index.d.ts.map +1 -1
  32. package/dist/react/index.js +3 -1
  33. package/dist/react/index.js.map +1 -1
  34. package/dist/react-core/hooks.d.ts +133 -0
  35. package/dist/react-core/hooks.d.ts.map +1 -1
  36. package/dist/react-core/index.js +85 -17
  37. package/dist/react-core/index.js.map +1 -1
  38. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
  39. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
  40. package/dist/react-native-core/hooks.d.ts +1 -1
  41. package/dist/react-native-core/hooks.d.ts.map +1 -1
  42. package/dist/react-native-core/index.js +3 -1
  43. package/dist/react-native-core/index.js.map +1 -1
  44. package/dist/testing.js +1 -1
  45. package/dist/tools/coValues/CoFieldInit.d.ts +5 -5
  46. package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
  47. package/dist/tools/coValues/CoValueBase.d.ts +14 -0
  48. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  49. package/dist/tools/coValues/coMap.d.ts +0 -12
  50. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  51. package/dist/tools/implementation/createContext.d.ts +2 -1
  52. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  53. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +5 -3
  54. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  55. package/dist/tools/tests/utils.d.ts.map +1 -1
  56. package/dist/worker/index.d.ts +4 -0
  57. package/dist/worker/index.d.ts.map +1 -1
  58. package/dist/worker/index.js +4 -2
  59. package/dist/worker/index.js.map +1 -1
  60. package/package.json +6 -4
  61. package/src/better-auth/auth/server.ts +8 -4
  62. package/src/better-auth/auth/tests/server.test.ts +2 -2
  63. package/src/inspector/index.tsx +1 -1
  64. package/src/inspector/ui/button.tsx +15 -1
  65. package/src/inspector/ui/heading.tsx +7 -2
  66. package/src/inspector/ui/input.tsx +6 -2
  67. package/src/inspector/ui/modal.tsx +158 -0
  68. package/src/inspector/viewer/delete-local-data.tsx +101 -0
  69. package/src/inspector/viewer/new-app.tsx +3 -1
  70. package/src/react/hooks.tsx +1 -0
  71. package/src/react/index.ts +1 -0
  72. package/src/react-core/hooks.ts +162 -0
  73. package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
  74. package/src/react-native-core/hooks.tsx +1 -0
  75. package/src/tools/coValues/CoFieldInit.ts +5 -5
  76. package/src/tools/coValues/CoValueBase.ts +32 -0
  77. package/src/tools/coValues/coList.ts +35 -0
  78. package/src/tools/coValues/coMap.ts +0 -18
  79. package/src/tools/implementation/createContext.ts +9 -2
  80. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +22 -8
  81. package/src/tools/tests/coList.test.ts +41 -0
  82. package/src/tools/tests/coMap.test.ts +37 -0
  83. package/src/tools/tests/coPlainText.test.ts +24 -0
  84. package/src/tools/tests/createContext.test.ts +24 -0
  85. package/src/tools/tests/deepLoading.test.ts +2 -0
  86. package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
  87. package/src/tools/tests/utils.ts +1 -0
  88. package/src/worker/index.ts +6 -0
  89. package/dist/chunk-45VKEOXG.js.map +0 -1
  90. package/dist/inspector/custom-element-IBHKHN27.js.map +0 -1
  91. /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -95,10 +95,14 @@ export const jazzPlugin: () => JazzPlugin = () => {
95
95
  contextContainsJazzAuth(context) &&
96
96
  verification.identifier.startsWith("sign-in-otp-")
97
97
  ) {
98
+ const identifier = `jazz-auth-${verification.identifier}`;
99
+ await context.context.internalAdapter.deleteVerificationByIdentifier(
100
+ identifier,
101
+ );
98
102
  await context.context.internalAdapter.createVerificationValue(
99
103
  {
100
104
  value: JSON.stringify({ jazzAuth: context.jazzAuth }),
101
- identifier: `${verification.identifier}-jazz-auth`,
105
+ identifier: identifier,
102
106
  expiresAt: verification.expiresAt,
103
107
  },
104
108
  );
@@ -166,7 +170,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
166
170
  handler: createAuthMiddleware(async (ctx) => {
167
171
  const state = ctx.query?.state || ctx.body?.state;
168
172
 
169
- const identifier = `${state}-jazz-auth`;
173
+ const identifier = `jazz-auth-${state}`;
170
174
 
171
175
  const data =
172
176
  await ctx.context.internalAdapter.findVerificationValue(
@@ -207,7 +211,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
207
211
  },
208
212
  handler: createAuthMiddleware(async (ctx) => {
209
213
  const email = ctx.body.email;
210
- const identifier = `sign-in-otp-${email}-jazz-auth`;
214
+ const identifier = `jazz-auth-sign-in-otp-${email}`;
211
215
 
212
216
  const data =
213
217
  await ctx.context.internalAdapter.findVerificationValue(
@@ -292,7 +296,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
292
296
 
293
297
  await ctx.context.internalAdapter.createVerificationValue({
294
298
  value,
295
- identifier: `${state}-jazz-auth`,
299
+ identifier: `jazz-auth-${state}`,
296
300
  expiresAt,
297
301
  });
298
302
  }),
@@ -293,7 +293,7 @@ describe("Better-Auth server plugin", async () => {
293
293
 
294
294
  expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
295
295
  expect(verificationCreationSpy.mock.calls[1]?.[0]).toMatchObject({
296
- identifier: expect.stringMatching("-jazz-auth"),
296
+ identifier: expect.stringMatching("jazz-auth-"),
297
297
  value: expect.stringContaining('"accountID":"123"'),
298
298
  });
299
299
  });
@@ -412,7 +412,7 @@ describe("Better-Auth server plugin", async () => {
412
412
  expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
413
413
  expect(verificationCreationSpy.mock.calls[0]?.[0]).toMatchObject(
414
414
  expect.objectContaining({
415
- identifier: "sign-in-otp-email@email.it-jazz-auth",
415
+ identifier: "jazz-auth-sign-in-otp-email@email.it",
416
416
  value: expect.stringContaining('"accountID":"123"'),
417
417
  }),
418
418
  );
@@ -23,7 +23,7 @@ import { useJazzContext } from "jazz-tools/react-core";
23
23
  import { Account } from "jazz-tools";
24
24
 
25
25
  import { JazzInspectorInternal } from "./viewer/new-app.js";
26
- import { Position } from "./viewer/inpsector-button.js";
26
+ import { Position } from "./viewer/inspector-button.js";
27
27
 
28
28
  export function JazzInspector({ position = "right" }: { position?: Position }) {
29
29
  const context = useJazzContext<Account>();
@@ -2,7 +2,7 @@ import { styled } from "goober";
2
2
  import { forwardRef } from "react";
3
3
 
4
4
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
- variant?: "primary" | "secondary" | "link" | "plain";
5
+ variant?: "primary" | "secondary" | "link" | "plain" | "destructive";
6
6
  children?: React.ReactNode;
7
7
  className?: string;
8
8
  disabled?: boolean;
@@ -29,6 +29,9 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
29
29
  border-color: var(--j-primary-color);
30
30
  color: white;
31
31
  font-weight: 500;
32
+ &:hover {
33
+ opacity: 0.8;
34
+ }
32
35
  `;
33
36
  case "secondary":
34
37
  return `
@@ -47,6 +50,17 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
47
50
  text-decoration: underline;
48
51
  }
49
52
  `;
53
+ case "destructive":
54
+ return `
55
+ padding: 0.375rem 0.75rem;
56
+ background-color: var(--j-destructive-color);
57
+ border-color: var(--j-destructive-color);
58
+ color: white;
59
+ font-weight: 500;
60
+ &:hover {
61
+ opacity: 0.8;
62
+ }
63
+ `;
50
64
  default:
51
65
  return "";
52
66
  }
@@ -10,6 +10,11 @@ const StyledHeading = styled("h1")<{ className?: string }>`
10
10
  export function Heading({
11
11
  children,
12
12
  className,
13
- }: React.PropsWithChildren<{ className?: string }>) {
14
- return <StyledHeading className={className}>{children}</StyledHeading>;
13
+ id,
14
+ }: React.PropsWithChildren<{ className?: string; id?: string }>) {
15
+ return (
16
+ <StyledHeading className={className} id={id}>
17
+ {children}
18
+ </StyledHeading>
19
+ );
15
20
  }
@@ -27,7 +27,7 @@ const StyledInput = styled("input")`
27
27
  box-shadow: var(--j-shadow-sm);
28
28
  font-weight: 500;
29
29
  background-color: white;
30
- color: var(--text-color-strong);
30
+ color: var(--j-text-color-strong);
31
31
 
32
32
  @media (prefers-color-scheme: dark) {
33
33
  background-color: var(--j-foreground);
@@ -41,7 +41,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
41
41
 
42
42
  return (
43
43
  <Container className={className}>
44
- <label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
44
+ <label
45
+ htmlFor={id}
46
+ className={hideLabel ? "j-sr-only" : ""}
47
+ style={{ color: "var(--j-text-color)" }}
48
+ >
45
49
  {label}
46
50
  </label>
47
51
  <StyledInput ref={ref} {...inputProps} id={id} />
@@ -0,0 +1,158 @@
1
+ import { styled } from "goober";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
+ import { Button } from "./button.js";
4
+ import { Heading } from "./heading.js";
5
+
6
+ interface ModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ heading: string;
10
+ text?: string;
11
+ children?: React.ReactNode;
12
+ confirmText?: string;
13
+ cancelText?: string;
14
+ onConfirm?: () => void;
15
+ onCancel?: () => void;
16
+ showButtons?: boolean;
17
+ className?: string;
18
+ }
19
+
20
+ const ModalContent = styled("dialog")`
21
+ background-color: var(--j-background);
22
+ border-radius: var(--j-radius-lg);
23
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
24
+ border: 1px solid var(--j-border-color);
25
+ max-width: 32rem;
26
+ margin-block: auto;
27
+ margin-inline: auto;
28
+ &::backdrop {
29
+ background-color: rgba(0, 0, 0, 0.7);
30
+ }
31
+
32
+ `;
33
+
34
+ const ModalHeader = styled("div")`
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: flex-start;
38
+ padding: 1.5rem 1.5rem 0 1.5rem;
39
+ gap: 1rem;
40
+ `;
41
+
42
+ const ModalBody = styled("div")`
43
+ padding: 1rem 1.5rem;
44
+ flex: 1;
45
+ `;
46
+
47
+ const ModalFooter = styled("div")`
48
+ display: flex;
49
+ justify-content: flex-end;
50
+ gap: 0.75rem;
51
+ padding: 0 1.5rem 1.5rem 1.5rem;
52
+ `;
53
+
54
+ const CloseButton = styled("button")`
55
+ background: none;
56
+ border: none;
57
+ cursor: pointer;
58
+ padding: 0.25rem;
59
+ border-radius: var(--j-radius-sm);
60
+ color: var(--j-text-color);
61
+ font-size: 1.25rem;
62
+ line-height: 1;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ min-width: 2rem;
67
+ min-height: 2rem;
68
+
69
+ &:hover {
70
+ background-color: var(--j-foreground);
71
+ }
72
+
73
+ &:focus-visible {
74
+ outline: 2px solid var(--j-border-focus);
75
+ outline-offset: 2px;
76
+ }
77
+ `;
78
+
79
+ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
80
+ (
81
+ {
82
+ isOpen,
83
+ onClose,
84
+ heading,
85
+ text,
86
+ children,
87
+ confirmText = "Confirm",
88
+ cancelText = "Cancel",
89
+ onConfirm,
90
+ onCancel,
91
+ showButtons = true,
92
+ className,
93
+ },
94
+ ref,
95
+ ) => {
96
+ const modalRef = useRef<HTMLDialogElement>(null);
97
+
98
+ useEffect(() => {
99
+ if (isOpen) {
100
+ modalRef.current?.showModal();
101
+ } else {
102
+ onClose();
103
+ modalRef.current?.close();
104
+ }
105
+ }, [isOpen, onClose]);
106
+
107
+ const handleConfirm = () => {
108
+ onConfirm?.();
109
+ onClose();
110
+ };
111
+
112
+ const handleCancel = () => {
113
+ onCancel?.();
114
+ onClose();
115
+ };
116
+
117
+ if (!isOpen) return null;
118
+
119
+ return (
120
+ <ModalContent
121
+ ref={ref || modalRef}
122
+ className={className}
123
+ role="dialog"
124
+ aria-labelledby="modal-heading"
125
+ onClose={onClose}
126
+ >
127
+ <ModalHeader>
128
+ <Heading id="modal-heading">{heading}</Heading>
129
+ <CloseButton onClick={onClose} aria-label="Close modal" type="button">
130
+ ×
131
+ </CloseButton>
132
+ </ModalHeader>
133
+
134
+ <ModalBody>
135
+ {text && (
136
+ <p style={{ margin: "0 0 1rem 0", color: "var(--j-text-color)" }}>
137
+ {text}
138
+ </p>
139
+ )}
140
+ {children}
141
+ </ModalBody>
142
+
143
+ {showButtons && (
144
+ <ModalFooter>
145
+ <Button variant="secondary" onClick={handleCancel}>
146
+ {cancelText}
147
+ </Button>
148
+ <Button variant="primary" onClick={handleConfirm}>
149
+ {confirmText}
150
+ </Button>
151
+ </ModalFooter>
152
+ )}
153
+ </ModalContent>
154
+ );
155
+ },
156
+ );
157
+
158
+ Modal.displayName = "Modal";
@@ -0,0 +1,101 @@
1
+ import { Button } from "../ui/button.js";
2
+ import { Modal } from "../ui/modal.js";
3
+ import { Input } from "../ui/input.js";
4
+ import { useState } from "react";
5
+
6
+ const DELETE_LOCAL_DATA_STRING = "delete my local data";
7
+
8
+ export function DeleteLocalData() {
9
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
10
+ const [confirmDeleteString, setConfirmDeleteString] = useState("");
11
+
12
+ return (
13
+ <>
14
+ <Button variant="destructive" onClick={() => setShowDeleteModal(true)}>
15
+ Delete my local data
16
+ </Button>
17
+ <Modal
18
+ isOpen={showDeleteModal}
19
+ onClose={() => setShowDeleteModal(false)}
20
+ heading="Delete Local Data"
21
+ showButtons={false}
22
+ >
23
+ <div
24
+ style={{
25
+ margin: "0 0 1rem 0",
26
+ color: "var(--j-text-color)",
27
+ display: "flex",
28
+ flexDirection: "column",
29
+ gap: "0.5rem",
30
+ }}
31
+ >
32
+ <p>
33
+ This action <strong>cannot</strong> be undone.
34
+ </p>
35
+ <p>
36
+ Be aware that the following data will be{" "}
37
+ <strong>permanently</strong> deleted:
38
+ </p>
39
+ <ul style={{ listStyleType: "disc", paddingLeft: "1rem" }}>
40
+ <li>
41
+ Unsynced data for <strong>all apps</strong> on{" "}
42
+ <code>{window.location.origin}</code>
43
+ </li>
44
+ <li>Accounts</li>
45
+ <li>Logged in sessions</li>
46
+ </ul>
47
+ <p></p>
48
+ </div>
49
+ <Input
50
+ label={`Type "${DELETE_LOCAL_DATA_STRING}" to confirm`}
51
+ placeholder={DELETE_LOCAL_DATA_STRING}
52
+ value={confirmDeleteString}
53
+ onChange={(e) => {
54
+ setConfirmDeleteString(e.target.value);
55
+ }}
56
+ />
57
+ <p
58
+ style={{
59
+ margin: "0 0 1rem 0",
60
+ color: "var(--j-text-color)",
61
+ display: "flex",
62
+ flexDirection: "column",
63
+ gap: "0.5rem",
64
+ }}
65
+ >
66
+ <small>
67
+ Data synced to a sync server will <strong>not</strong> be deleted,
68
+ and will be synced when you log in again.
69
+ </small>
70
+ </p>
71
+ <div
72
+ style={{
73
+ display: "flex",
74
+ marginTop: "0.5rem",
75
+ justifyContent: "flex-end",
76
+ gap: "0.5rem",
77
+ }}
78
+ >
79
+ <Button variant="secondary" onClick={() => setShowDeleteModal(false)}>
80
+ Cancel
81
+ </Button>
82
+ <Button
83
+ variant="destructive"
84
+ disabled={confirmDeleteString !== DELETE_LOCAL_DATA_STRING}
85
+ onClick={() => {
86
+ const jazzKeys = Object.keys(localStorage).filter(
87
+ (key) => key.startsWith("jazz-") || key.startsWith("co_z"),
88
+ );
89
+ jazzKeys.forEach((key) => localStorage.removeItem(key));
90
+ indexedDB.deleteDatabase("jazz-storage");
91
+ window.location.reload();
92
+ setShowDeleteModal(false);
93
+ }}
94
+ >
95
+ I'm sure, delete my local data
96
+ </Button>
97
+ </div>
98
+ </Modal>
99
+ </>
100
+ );
101
+ }
@@ -9,8 +9,9 @@ import { usePagePath } from "./use-page-path.js";
9
9
 
10
10
  import { GlobalStyles } from "../ui/global-styles.js";
11
11
  import { Heading } from "../ui/heading.js";
12
- import { InspectorButton, type Position } from "./inpsector-button.js";
12
+ import { InspectorButton, type Position } from "./inspector-button.js";
13
13
  import { useOpenInspector } from "./use-open-inspector.js";
14
+ import { DeleteLocalData } from "./delete-local-data.js";
14
15
 
15
16
  const InspectorContainer = styled("div")`
16
17
  position: fixed;
@@ -102,6 +103,7 @@ export function JazzInspectorInternal({
102
103
  />
103
104
  )}
104
105
  </Form>
106
+ <DeleteLocalData />
105
107
  <Button variant="plain" type="button" onClick={() => setOpen(false)}>
106
108
  Close
107
109
  </Button>
@@ -50,4 +50,5 @@ export {
50
50
  experimental_useInboxSender,
51
51
  useJazzContext,
52
52
  useAccount,
53
+ useCoStateWithSelector,
53
54
  } from "jazz-tools/react-core";
@@ -7,6 +7,7 @@ export {
7
7
  experimental_useInboxSender,
8
8
  useJazzContext,
9
9
  useAuthSecretStorage,
10
+ useCoStateWithSelector,
10
11
  } from "./hooks.js";
11
12
 
12
13
  export { createInviteLink, parseInviteLink } from "jazz-tools/browser";
@@ -1,3 +1,4 @@
1
+ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
1
2
  import React, {
2
3
  useCallback,
3
4
  useContext,
@@ -261,6 +262,167 @@ export function useCoState<
261
262
  return value;
262
263
  }
263
264
 
265
+ /**
266
+ * React hook for subscribing to CoValues with selective data extraction and custom equality checking.
267
+ *
268
+ * This hook extends `useCoState` by allowing you to select only specific parts of the CoValue data
269
+ * through a selector function, which helps reduce unnecessary re-renders by narrowing down the
270
+ * returned data. Additionally, you can provide a custom equality function to further optimize
271
+ * performance by controlling when the component should re-render based on the selected data.
272
+ *
273
+ * The hook automatically handles the subscription lifecycle and supports deep loading of nested
274
+ * CoValues through resolve queries, just like `useCoState`.
275
+ *
276
+ * @returns The result of the selector function applied to the loaded CoValue data
277
+ *
278
+ * @example
279
+ * ```tsx
280
+ * // Select only specific fields to reduce re-renders
281
+ * const Project = co.map({
282
+ * name: z.string(),
283
+ * description: z.string(),
284
+ * tasks: co.list(Task),
285
+ * lastModified: z.date(),
286
+ * });
287
+ *
288
+ * function ProjectTitle({ projectId }: { projectId: string }) {
289
+ * // Only re-render when the project name changes, not other fields
290
+ * const projectName = useCoStateWithSelector(
291
+ * Project,
292
+ * projectId,
293
+ * {
294
+ * select: (project) => project?.name ?? "Loading...",
295
+ * }
296
+ * );
297
+ *
298
+ * return <h1>{projectName}</h1>;
299
+ * }
300
+ * ```
301
+ *
302
+ * @example
303
+ * ```tsx
304
+ * // Use custom equality function for complex data structures
305
+ * const TaskList = co.list(Task);
306
+ *
307
+ * function TaskCount({ listId }: { listId: string }) {
308
+ * const taskStats = useCoStateWithSelector(
309
+ * TaskList,
310
+ * listId,
311
+ * {
312
+ * resolve: { $each: true },
313
+ * select: (tasks) => {
314
+ * if (!tasks) return { total: 0, completed: 0 };
315
+ * return {
316
+ * total: tasks.length,
317
+ * completed: tasks.filter(task => task.completed).length,
318
+ * };
319
+ * },
320
+ * // Custom equality to prevent re-renders when stats haven't changed
321
+ * equalityFn: (a, b) => a.total === b.total && a.completed === b.completed,
322
+ * }
323
+ * );
324
+ *
325
+ * return (
326
+ * <div>
327
+ * {taskStats.completed} of {taskStats.total} tasks completed
328
+ * </div>
329
+ * );
330
+ * }
331
+ * ```
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * // Combine with deep loading and complex selectors
336
+ * const Team = co.map({
337
+ * name: z.string(),
338
+ * members: co.list(TeamMember),
339
+ * projects: co.list(Project),
340
+ * });
341
+ *
342
+ * function TeamSummary({ teamId }: { teamId: string }) {
343
+ * const summary = useCoStateWithSelector(
344
+ * Team,
345
+ * teamId,
346
+ * {
347
+ * resolve: {
348
+ * members: { $each: true },
349
+ * projects: { $each: { tasks: { $each: true } } },
350
+ * },
351
+ * select: (team) => {
352
+ * if (!team) return null;
353
+ *
354
+ * const totalTasks = team.projects.reduce(
355
+ * (sum, project) => sum + project.tasks.length,
356
+ * 0
357
+ * );
358
+ *
359
+ * return {
360
+ * teamName: team.name,
361
+ * memberCount: team.members.length,
362
+ * projectCount: team.projects.length,
363
+ * totalTasks,
364
+ * };
365
+ * },
366
+ * }
367
+ * );
368
+ *
369
+ * if (!summary) return <div>Loading team summary...</div>;
370
+ *
371
+ * return (
372
+ * <div>
373
+ * <h2>{summary.teamName}</h2>
374
+ * <p>{summary.memberCount} members</p>
375
+ * <p>{summary.projectCount} projects</p>
376
+ * <p>{summary.totalTasks} total tasks</p>
377
+ * </div>
378
+ * );
379
+ * }
380
+ * ```
381
+ *
382
+ * For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
383
+ */
384
+ export function useCoStateWithSelector<
385
+ S extends CoValueClassOrSchema,
386
+ TSelectorReturn,
387
+ const R extends ResolveQuery<S> = true,
388
+ >(
389
+ /** The CoValue schema or class constructor */
390
+ Schema: S,
391
+ /** The ID of the CoValue to subscribe to. If `undefined`, returns the result of selector called with `null` */
392
+ id: string | undefined,
393
+ /** Optional configuration for the subscription */
394
+ options: {
395
+ /** Resolve query to specify which nested CoValues to load */
396
+ resolve?: ResolveQueryStrict<S, R>;
397
+ /** Select which value to return */
398
+ select: (value: Loaded<S, R> | undefined | null) => TSelectorReturn;
399
+ /** Equality function to determine if the selected value has changed, defaults to `Object.is` */
400
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
401
+ },
402
+ ): TSelectorReturn {
403
+ const subscription = useCoValueSubscription(Schema, id, options);
404
+
405
+ return useSyncExternalStoreWithSelector<
406
+ Loaded<S, R> | undefined | null,
407
+ TSelectorReturn
408
+ >(
409
+ React.useCallback(
410
+ (callback) => {
411
+ if (!subscription) {
412
+ return () => {};
413
+ }
414
+
415
+ return subscription.subscribe(callback);
416
+ },
417
+ [subscription],
418
+ ),
419
+ () => (subscription ? subscription.getCurrentValue() : null),
420
+ () => (subscription ? subscription.getCurrentValue() : null),
421
+ options.select,
422
+ options.equalityFn ?? Object.is,
423
+ );
424
+ }
425
+
264
426
  function useAccountSubscription<
265
427
  S extends AccountClass<Account> | AnyAccountSchema,
266
428
  const R extends ResolveQuery<S>,