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.
- package/.turbo/turbo-build.log +57 -57
- package/CHANGELOG.md +33 -0
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js +7 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/better-auth/auth/react.d.ts +0 -2145
- package/dist/better-auth/auth/react.d.ts.map +1 -1
- package/dist/better-auth/auth/react.js +2 -14
- package/dist/better-auth/auth/react.js.map +1 -1
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +77 -22
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/auth/tests/react.test.d.ts +2 -0
- package/dist/better-auth/auth/tests/react.test.d.ts.map +1 -0
- package/dist/{chunk-3LE7N6TH.js → chunk-CFAY3FMQ.js} +192 -101
- package/dist/chunk-CFAY3FMQ.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/{custom-element-WCY6D3QJ.js → custom-element-G6SPZEBR.js} +308 -97
- package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
- package/dist/inspector/index.d.ts +5 -1
- package/dist/inspector/index.d.ts.map +1 -1
- package/dist/inspector/index.js +318 -56
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/ui/button.d.ts +1 -1
- package/dist/inspector/ui/button.d.ts.map +1 -1
- package/dist/inspector/ui/heading.d.ts +2 -1
- package/dist/inspector/ui/heading.d.ts.map +1 -1
- package/dist/inspector/ui/input.d.ts.map +1 -1
- package/dist/inspector/ui/modal.d.ts +16 -0
- package/dist/inspector/ui/modal.d.ts.map +1 -0
- package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
- package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
- package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
- package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
- package/dist/inspector/viewer/new-app.d.ts +1 -4
- package/dist/inspector/viewer/new-app.d.ts.map +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +133 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +83 -17
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/CoValueBase.d.ts +14 -0
- package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +0 -12
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/coValues/inbox.d.ts +5 -5
- package/dist/tools/coValues/inbox.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +2 -1
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/tests/utils.d.ts.map +1 -1
- package/dist/worker/index.d.ts +12 -2
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +10 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -4
- package/src/better-auth/auth/client.ts +8 -2
- package/src/better-auth/auth/react.tsx +2 -51
- package/src/better-auth/auth/server.ts +98 -24
- package/src/better-auth/auth/tests/client.test.ts +92 -4
- package/src/better-auth/auth/tests/react.test.tsx +43 -0
- package/src/better-auth/auth/tests/server.test.ts +276 -98
- package/src/inspector/custom-element.tsx +1 -1
- package/src/inspector/index.tsx +44 -0
- package/src/inspector/ui/button.tsx +15 -1
- package/src/inspector/ui/heading.tsx +7 -2
- package/src/inspector/ui/input.tsx +6 -2
- package/src/inspector/ui/modal.tsx +158 -0
- package/src/inspector/viewer/delete-local-data.tsx +101 -0
- package/src/inspector/viewer/new-app.tsx +3 -19
- package/src/react/hooks.tsx +1 -0
- package/src/react/index.ts +1 -0
- package/src/react-core/hooks.ts +162 -0
- package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
- package/src/react-native-core/hooks.tsx +1 -0
- package/src/tools/coValues/CoValueBase.ts +32 -0
- package/src/tools/coValues/coList.ts +35 -0
- package/src/tools/coValues/coMap.ts +0 -18
- package/src/tools/coValues/inbox.ts +190 -108
- package/src/tools/implementation/createContext.ts +9 -2
- package/src/tools/testing.ts +1 -1
- package/src/tools/tests/coFeed.test.ts +33 -22
- package/src/tools/tests/coList.test.ts +47 -4
- package/src/tools/tests/coMap.test.ts +13 -5
- package/src/tools/tests/coPlainText.test.ts +24 -0
- package/src/tools/tests/createContext.test.ts +24 -0
- package/src/tools/tests/deepLoading.test.ts +2 -0
- package/src/tools/tests/exportImport.test.ts +3 -1
- package/src/tools/tests/groupsAndAccounts.test.ts +56 -44
- package/src/tools/tests/inbox.test.ts +293 -31
- package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
- package/src/tools/tests/utils.ts +1 -0
- package/src/worker/index.ts +21 -5
- package/tsup.config.ts +1 -1
- package/dist/chunk-3LE7N6TH.js.map +0 -1
- package/dist/inspector/custom-element-WCY6D3QJ.js.map +0 -1
- package/src/inspector/index.ts +0 -23
- /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 "./
|
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>
|
package/src/react/hooks.tsx
CHANGED
package/src/react/index.ts
CHANGED
package/src/react-core/hooks.ts
CHANGED
@@ -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
|
+
});
|
@@ -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;
|