jazz-tools 0.18.5 → 0.18.7

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 (111) hide show
  1. package/.turbo/turbo-build.log +57 -57
  2. package/CHANGELOG.md +33 -0
  3. package/dist/better-auth/auth/client.d.ts.map +1 -1
  4. package/dist/better-auth/auth/client.js +7 -1
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/better-auth/auth/react.d.ts +0 -2145
  7. package/dist/better-auth/auth/react.d.ts.map +1 -1
  8. package/dist/better-auth/auth/react.js +2 -14
  9. package/dist/better-auth/auth/react.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +77 -22
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
  14. package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
  15. package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
  16. package/dist/chunk-CFAY3FMQ.js.map +1 -0
  17. package/dist/index.js +1 -1
  18. package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
  19. package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
  20. package/dist/inspector/index.d.ts +5 -1
  21. package/dist/inspector/index.d.ts.map +1 -1
  22. package/dist/inspector/index.js +318 -56
  23. package/dist/inspector/index.js.map +1 -1
  24. package/dist/inspector/register-custom-element.js +1 -1
  25. package/dist/inspector/ui/button.d.ts +1 -1
  26. package/dist/inspector/ui/button.d.ts.map +1 -1
  27. package/dist/inspector/ui/heading.d.ts +2 -1
  28. package/dist/inspector/ui/heading.d.ts.map +1 -1
  29. package/dist/inspector/ui/input.d.ts.map +1 -1
  30. package/dist/inspector/ui/modal.d.ts +16 -0
  31. package/dist/inspector/ui/modal.d.ts.map +1 -0
  32. package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
  33. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
  34. package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
  35. package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
  36. package/dist/inspector/viewer/new-app.d.ts +1 -4
  37. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  38. package/dist/react/hooks.d.ts +1 -1
  39. package/dist/react/hooks.d.ts.map +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -1
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react-core/hooks.d.ts +133 -0
  45. package/dist/react-core/hooks.d.ts.map +1 -1
  46. package/dist/react-core/index.js +83 -17
  47. package/dist/react-core/index.js.map +1 -1
  48. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
  49. package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
  50. package/dist/react-native-core/hooks.d.ts +1 -1
  51. package/dist/react-native-core/hooks.d.ts.map +1 -1
  52. package/dist/react-native-core/index.js +3 -1
  53. package/dist/react-native-core/index.js.map +1 -1
  54. package/dist/testing.js +2 -2
  55. package/dist/testing.js.map +1 -1
  56. package/dist/tools/coValues/CoValueBase.d.ts +14 -0
  57. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  58. package/dist/tools/coValues/coMap.d.ts +0 -12
  59. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  60. package/dist/tools/coValues/inbox.d.ts +5 -5
  61. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  62. package/dist/tools/implementation/createContext.d.ts +2 -1
  63. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  64. package/dist/tools/tests/utils.d.ts.map +1 -1
  65. package/dist/worker/index.d.ts +12 -2
  66. package/dist/worker/index.d.ts.map +1 -1
  67. package/dist/worker/index.js +10 -4
  68. package/dist/worker/index.js.map +1 -1
  69. package/package.json +6 -4
  70. package/src/better-auth/auth/client.ts +8 -2
  71. package/src/better-auth/auth/react.tsx +2 -51
  72. package/src/better-auth/auth/server.ts +98 -24
  73. package/src/better-auth/auth/tests/client.test.ts +92 -4
  74. package/src/better-auth/auth/tests/react.test.tsx +43 -0
  75. package/src/better-auth/auth/tests/server.test.ts +276 -98
  76. package/src/inspector/custom-element.tsx +1 -1
  77. package/src/inspector/index.tsx +44 -0
  78. package/src/inspector/ui/button.tsx +15 -1
  79. package/src/inspector/ui/heading.tsx +7 -2
  80. package/src/inspector/ui/input.tsx +6 -2
  81. package/src/inspector/ui/modal.tsx +158 -0
  82. package/src/inspector/viewer/delete-local-data.tsx +101 -0
  83. package/src/inspector/viewer/new-app.tsx +3 -19
  84. package/src/react/hooks.tsx +1 -0
  85. package/src/react/index.ts +1 -0
  86. package/src/react-core/hooks.ts +162 -0
  87. package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
  88. package/src/react-native-core/hooks.tsx +1 -0
  89. package/src/tools/coValues/CoValueBase.ts +32 -0
  90. package/src/tools/coValues/coList.ts +35 -0
  91. package/src/tools/coValues/coMap.ts +0 -18
  92. package/src/tools/coValues/inbox.ts +190 -108
  93. package/src/tools/implementation/createContext.ts +9 -2
  94. package/src/tools/testing.ts +1 -1
  95. package/src/tools/tests/coFeed.test.ts +33 -22
  96. package/src/tools/tests/coList.test.ts +47 -4
  97. package/src/tools/tests/coMap.test.ts +13 -5
  98. package/src/tools/tests/coPlainText.test.ts +24 -0
  99. package/src/tools/tests/createContext.test.ts +24 -0
  100. package/src/tools/tests/deepLoading.test.ts +2 -0
  101. package/src/tools/tests/exportImport.test.ts +3 -1
  102. package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
  103. package/src/tools/tests/inbox.test.ts +293 -31
  104. package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
  105. package/src/tools/tests/utils.ts +1 -0
  106. package/src/worker/index.ts +21 -5
  107. package/tsup.config.ts +1 -1
  108. package/dist/chunk-3LE7N6TH.js.map +0 -1
  109. package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
  110. package/src/inspector/index.ts +0 -23
  111. /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -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
+ }
@@ -1,6 +1,5 @@
1
1
  import { CoID, LocalNode, RawAccount, RawCoValue } from "cojson";
2
2
  import { styled } from "goober";
3
- import { useJazzContext } from "jazz-tools/react-core";
4
3
  import React, { useState } from "react";
5
4
  import { Button } from "../ui/button.js";
6
5
  import { Input } from "../ui/input.js";
@@ -8,11 +7,11 @@ import { Breadcrumbs } from "./breadcrumbs.js";
8
7
  import { PageStack } from "./page-stack.js";
9
8
  import { usePagePath } from "./use-page-path.js";
10
9
 
11
- import { Account } from "jazz-tools";
12
10
  import { GlobalStyles } from "../ui/global-styles.js";
13
11
  import { Heading } from "../ui/heading.js";
14
- import { InspectorButton, type Position } from "./inpsector-button.js";
12
+ import { InspectorButton, type Position } from "./inspector-button.js";
15
13
  import { useOpenInspector } from "./use-open-inspector.js";
14
+ import { DeleteLocalData } from "./delete-local-data.js";
16
15
 
17
16
  const InspectorContainer = styled("div")`
18
17
  position: fixed;
@@ -61,22 +60,6 @@ const OrText = styled("p")`
61
60
  text-align: center;
62
61
  `;
63
62
 
64
- export function JazzInspector({ position = "right" }: { position?: Position }) {
65
- const context = useJazzContext<Account>();
66
- const localNode = context.node;
67
- const me = "me" in context ? context.me : undefined;
68
-
69
- if (process.env.NODE_ENV !== "development") return null;
70
-
71
- return (
72
- <JazzInspectorInternal
73
- position={position}
74
- localNode={localNode}
75
- accountId={me?.$jazz.raw.id}
76
- />
77
- );
78
- }
79
-
80
63
  export function JazzInspectorInternal({
81
64
  position = "right",
82
65
  localNode,
@@ -120,6 +103,7 @@ export function JazzInspectorInternal({
120
103
  />
121
104
  )}
122
105
  </Form>
106
+ <DeleteLocalData />
123
107
  <Button variant="plain" type="button" onClick={() => setOpen(false)}>
124
108
  Close
125
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>,
@@ -0,0 +1,149 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { cojsonInternals } from "cojson";
4
+ import { Account, co, Loaded, z } from "jazz-tools";
5
+ import { beforeEach, describe, expect, expectTypeOf, it } from "vitest";
6
+ import { useCoStateWithSelector } from "../index.js";
7
+ import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
8
+ import { renderHook, waitFor } from "./testUtils.js";
9
+ import { useRef } from "react";
10
+
11
+ beforeEach(async () => {
12
+ await setupJazzTestSync();
13
+
14
+ await createJazzTestAccount({
15
+ isCurrentActiveAccount: true,
16
+ });
17
+ });
18
+
19
+ cojsonInternals.setCoValueLoadingRetryDelay(300);
20
+
21
+ const useRenderCount = <T>(hook: () => T) => {
22
+ const renderCountRef = useRef(0);
23
+ const result = hook();
24
+ renderCountRef.current = renderCountRef.current + 1;
25
+ return {
26
+ renderCount: renderCountRef.current,
27
+ result,
28
+ };
29
+ };
30
+
31
+ describe("useCoStateWithSelector", () => {
32
+ it("should not re-render when a nested coValue is updated and not selected", async () => {
33
+ const TestMap = co.map({
34
+ value: z.string(),
35
+ get nested() {
36
+ return TestMap.optional();
37
+ },
38
+ });
39
+
40
+ const map = TestMap.create({
41
+ value: "1",
42
+ nested: TestMap.create({
43
+ value: "1",
44
+ }),
45
+ });
46
+
47
+ const { result } = renderHook(() =>
48
+ useRenderCount(() =>
49
+ useCoStateWithSelector(TestMap, map.$jazz.id, {
50
+ resolve: {
51
+ nested: true,
52
+ },
53
+ select: (v) => v?.value,
54
+ }),
55
+ ),
56
+ );
57
+
58
+ await waitFor(() => {
59
+ expect(result.current.result).not.toBeUndefined();
60
+ });
61
+
62
+ for (let i = 0; i < 100; i++) {
63
+ map.nested!.$jazz.set("value", `${i}`);
64
+ await Account.getMe().$jazz.waitForAllCoValuesSync();
65
+ }
66
+
67
+ expect(result.current.result).toEqual("1");
68
+ expect(result.current.renderCount).toEqual(1);
69
+ });
70
+
71
+ it("should re-render when a nested coValue is updated and selected", async () => {
72
+ const TestMap = co.map({
73
+ value: z.string(),
74
+ get nested() {
75
+ return TestMap.optional();
76
+ },
77
+ });
78
+
79
+ const map = TestMap.create({
80
+ value: "1",
81
+ nested: TestMap.create({
82
+ value: "1",
83
+ }),
84
+ });
85
+
86
+ const { result } = renderHook(() =>
87
+ useRenderCount(() =>
88
+ useCoStateWithSelector(TestMap, map.$jazz.id, {
89
+ resolve: {
90
+ nested: true,
91
+ },
92
+ select: (v) => v?.nested?.value,
93
+ }),
94
+ ),
95
+ );
96
+
97
+ await waitFor(() => {
98
+ expect(result.current.result).not.toBeUndefined();
99
+ });
100
+
101
+ for (let i = 1; i <= 100; i++) {
102
+ map.nested!.$jazz.set("value", `${i}`);
103
+ await Account.getMe().$jazz.waitForAllCoValuesSync();
104
+ }
105
+
106
+ expect(result.current.result).toEqual("100");
107
+
108
+ // skips re-render on i = 1, only re-renders on i = [2,100], so initial render + 99 renders = 100
109
+ expect(result.current.renderCount).toEqual(100);
110
+
111
+ expectTypeOf(result.current.result).toEqualTypeOf<string | undefined>();
112
+ });
113
+
114
+ it("should not re-render when equalityFn always returns true", async () => {
115
+ const TestMap = co.map({
116
+ value: z.string(),
117
+ get nested() {
118
+ return TestMap.optional();
119
+ },
120
+ });
121
+
122
+ const map = TestMap.create({
123
+ value: "1",
124
+ nested: TestMap.create({
125
+ value: "1",
126
+ }),
127
+ });
128
+
129
+ const { result } = renderHook(() =>
130
+ useRenderCount(() =>
131
+ useCoStateWithSelector(TestMap, map.$jazz.id, {
132
+ resolve: {
133
+ nested: true,
134
+ },
135
+ select: (v) => v?.nested?.value,
136
+ equalityFn: () => true,
137
+ }),
138
+ ),
139
+ );
140
+
141
+ for (let i = 1; i <= 100; i++) {
142
+ map.nested!.$jazz.set("value", `${i}`);
143
+ await Account.getMe().$jazz.waitForAllCoValuesSync();
144
+ }
145
+
146
+ expect(result.current.result).toEqual("1");
147
+ expect(result.current.renderCount).toEqual(1);
148
+ });
149
+ });
@@ -13,6 +13,7 @@ export {
13
13
  useAuthSecretStorage,
14
14
  useIsAuthenticated,
15
15
  useAccount,
16
+ useCoStateWithSelector,
16
17
  } from "jazz-tools/react-core";
17
18
 
18
19
  export function useAcceptInviteNative<S extends CoValueClassOrSchema>({
@@ -77,4 +77,36 @@ export abstract class CoValueJazzApi<V extends CoValue> {
77
77
 
78
78
  return new AnonymousJazzAgent(this.localNode);
79
79
  }
80
+
81
+ /**
82
+ * The timestamp of the creation time of the CoValue
83
+ *
84
+ * @category Content
85
+ */
86
+ get createdAt(): number {
87
+ const createdAt = this.raw.core.verified.header.meta?.createdAt;
88
+
89
+ if (typeof createdAt === "string") {
90
+ return new Date(createdAt).getTime();
91
+ }
92
+
93
+ return this.raw.core.earliestTxMadeAt;
94
+ }
95
+
96
+ /**
97
+ * The timestamp of the last updated time of the CoValue
98
+ *
99
+ * Returns the creation time if there are no updates.
100
+ *
101
+ * @category Content
102
+ */
103
+ get lastUpdatedAt(): number {
104
+ const value = this.raw.core.latestTxMadeAt;
105
+
106
+ if (value === 0) {
107
+ return this.createdAt;
108
+ }
109
+
110
+ return value;
111
+ }
80
112
  }
@@ -971,4 +971,39 @@ const CoListProxyHandler: ProxyHandler<CoList> = {
971
971
  return Reflect.has(target, key);
972
972
  }
973
973
  },
974
+ ownKeys(target) {
975
+ const keys = Reflect.ownKeys(target);
976
+ // Add numeric indices for all entries in the list
977
+ const indexKeys = target.$jazz.raw.entries().map((_entry, i) => String(i));
978
+ keys.push(...indexKeys);
979
+ return keys;
980
+ },
981
+ getOwnPropertyDescriptor(target, key) {
982
+ if (key === TypeSym) {
983
+ // Make TypeSym non-enumerable so it doesn't show up in Object.keys()
984
+ return {
985
+ enumerable: false,
986
+ configurable: true,
987
+ writable: false,
988
+ value: target[TypeSym],
989
+ };
990
+ } else if (key in target) {
991
+ return Reflect.getOwnPropertyDescriptor(target, key);
992
+ } else if (typeof key === "string" && !isNaN(+key)) {
993
+ const index = Number(key);
994
+ if (index >= 0 && index < target.$jazz.raw.entries().length) {
995
+ return {
996
+ enumerable: true,
997
+ configurable: true,
998
+ writable: true,
999
+ };
1000
+ }
1001
+ } else if (key === "length") {
1002
+ return {
1003
+ enumerable: false,
1004
+ configurable: false,
1005
+ writable: false,
1006
+ };
1007
+ }
1008
+ },
974
1009
  };
@@ -836,24 +836,6 @@ class CoMapJazzApi<M extends CoMap> extends CoValueJazzApi<M> {
836
836
  return this.getRaw();
837
837
  }
838
838
 
839
- /**
840
- * The timestamp of the creation time of the CoMap
841
- *
842
- * @category Content
843
- */
844
- get createdAt(): number {
845
- return this.raw.earliestTxMadeAt ?? Number.MAX_SAFE_INTEGER;
846
- }
847
-
848
- /**
849
- * The timestamp of the last updated time of the CoMap
850
- *
851
- * @category Content
852
- */
853
- get lastUpdatedAt(): number {
854
- return this.raw.latestTxMadeAt;
855
- }
856
-
857
839
  /** @internal */
858
840
  get schema(): CoMapFieldSchema {
859
841
  return (this.coMap.constructor as typeof CoMap)._schema;