jazz-tools 0.19.8 → 0.19.11

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 (154) hide show
  1. package/.turbo/turbo-build.log +56 -50
  2. package/CHANGELOG.md +30 -3
  3. package/dist/{chunk-2S3Z2CN6.js → chunk-HX5S6W5E.js} +372 -103
  4. package/dist/chunk-HX5S6W5E.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/account-switcher.d.ts +4 -0
  7. package/dist/inspector/account-switcher.d.ts.map +1 -0
  8. package/dist/inspector/chunk-C6BJPHBQ.js +4096 -0
  9. package/dist/inspector/chunk-C6BJPHBQ.js.map +1 -0
  10. package/dist/inspector/contexts/node.d.ts +19 -0
  11. package/dist/inspector/contexts/node.d.ts.map +1 -0
  12. package/dist/inspector/{custom-element-P76EIWEV.js → custom-element-GJVBPZES.js} +1011 -884
  13. package/dist/inspector/custom-element-GJVBPZES.js.map +1 -0
  14. package/dist/inspector/{viewer/new-app.d.ts → in-app.d.ts} +3 -3
  15. package/dist/inspector/in-app.d.ts.map +1 -0
  16. package/dist/inspector/index.d.ts +0 -11
  17. package/dist/inspector/index.d.ts.map +1 -1
  18. package/dist/inspector/index.js +56 -3910
  19. package/dist/inspector/index.js.map +1 -1
  20. package/dist/inspector/pages/home.d.ts +2 -0
  21. package/dist/inspector/pages/home.d.ts.map +1 -0
  22. package/dist/inspector/register-custom-element.js +1 -1
  23. package/dist/inspector/router/context.d.ts +12 -0
  24. package/dist/inspector/router/context.d.ts.map +1 -0
  25. package/dist/inspector/router/hash-router.d.ts +7 -0
  26. package/dist/inspector/router/hash-router.d.ts.map +1 -0
  27. package/dist/inspector/router/in-memory-router.d.ts +7 -0
  28. package/dist/inspector/router/in-memory-router.d.ts.map +1 -0
  29. package/dist/inspector/router/index.d.ts +5 -0
  30. package/dist/inspector/router/index.d.ts.map +1 -0
  31. package/dist/inspector/standalone.d.ts +6 -0
  32. package/dist/inspector/standalone.d.ts.map +1 -0
  33. package/dist/inspector/standalone.js +420 -0
  34. package/dist/inspector/standalone.js.map +1 -0
  35. package/dist/inspector/tests/router/hash-router.test.d.ts +2 -0
  36. package/dist/inspector/tests/router/hash-router.test.d.ts.map +1 -0
  37. package/dist/inspector/tests/router/in-memory-router.test.d.ts +2 -0
  38. package/dist/inspector/tests/router/in-memory-router.test.d.ts.map +1 -0
  39. package/dist/inspector/ui/modal.d.ts +1 -0
  40. package/dist/inspector/ui/modal.d.ts.map +1 -1
  41. package/dist/inspector/viewer/breadcrumbs.d.ts +1 -7
  42. package/dist/inspector/viewer/breadcrumbs.d.ts.map +1 -1
  43. package/dist/inspector/viewer/header.d.ts +7 -0
  44. package/dist/inspector/viewer/header.d.ts.map +1 -0
  45. package/dist/inspector/viewer/page-stack.d.ts +4 -13
  46. package/dist/inspector/viewer/page-stack.d.ts.map +1 -1
  47. package/dist/inspector/viewer/page.d.ts.map +1 -1
  48. package/dist/react/hooks.d.ts +1 -1
  49. package/dist/react/hooks.d.ts.map +1 -1
  50. package/dist/react/index.d.ts +1 -1
  51. package/dist/react/index.d.ts.map +1 -1
  52. package/dist/react/index.js +5 -1
  53. package/dist/react/index.js.map +1 -1
  54. package/dist/react-core/hooks.d.ts +59 -0
  55. package/dist/react-core/hooks.d.ts.map +1 -1
  56. package/dist/react-core/index.js +124 -36
  57. package/dist/react-core/index.js.map +1 -1
  58. package/dist/react-core/tests/testUtils.d.ts +1 -0
  59. package/dist/react-core/tests/testUtils.d.ts.map +1 -1
  60. package/dist/react-core/tests/useSuspenseAccount.test.d.ts +2 -0
  61. package/dist/react-core/tests/useSuspenseAccount.test.d.ts.map +1 -0
  62. package/dist/react-core/tests/useSuspenseCoState.test.d.ts +2 -0
  63. package/dist/react-core/tests/useSuspenseCoState.test.d.ts.map +1 -0
  64. package/dist/react-core/use.d.ts +3 -0
  65. package/dist/react-core/use.d.ts.map +1 -0
  66. package/dist/react-native/index.js +5 -1
  67. package/dist/react-native/index.js.map +1 -1
  68. package/dist/react-native-core/crypto/RNCrypto.d.ts +2 -0
  69. package/dist/react-native-core/crypto/RNCrypto.d.ts.map +1 -0
  70. package/dist/react-native-core/crypto/RNCrypto.js +3 -0
  71. package/dist/react-native-core/crypto/RNCrypto.js.map +1 -0
  72. package/dist/react-native-core/hooks.d.ts +1 -1
  73. package/dist/react-native-core/hooks.d.ts.map +1 -1
  74. package/dist/react-native-core/index.js +5 -1
  75. package/dist/react-native-core/index.js.map +1 -1
  76. package/dist/react-native-core/platform.d.ts +2 -1
  77. package/dist/react-native-core/platform.d.ts.map +1 -1
  78. package/dist/testing.js +1 -1
  79. package/dist/testing.js.map +1 -1
  80. package/dist/tools/coValues/account.d.ts +7 -1
  81. package/dist/tools/coValues/account.d.ts.map +1 -1
  82. package/dist/tools/coValues/interfaces.d.ts +1 -1
  83. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  84. package/dist/tools/implementation/ContextManager.d.ts +3 -0
  85. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  86. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +8 -1
  87. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  88. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  89. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +8 -22
  90. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  91. package/dist/tools/subscribe/SubscriptionCache.d.ts +51 -0
  92. package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -0
  93. package/dist/tools/subscribe/SubscriptionScope.d.ts +17 -1
  94. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  95. package/dist/tools/subscribe/utils.d.ts +9 -1
  96. package/dist/tools/subscribe/utils.d.ts.map +1 -1
  97. package/dist/tools/testing.d.ts +2 -2
  98. package/dist/tools/testing.d.ts.map +1 -1
  99. package/dist/tools/tests/SubscriptionCache.test.d.ts +2 -0
  100. package/dist/tools/tests/SubscriptionCache.test.d.ts.map +1 -0
  101. package/package.json +18 -6
  102. package/src/inspector/account-switcher.tsx +440 -0
  103. package/src/inspector/contexts/node.tsx +129 -0
  104. package/src/inspector/custom-element.tsx +2 -2
  105. package/src/inspector/in-app.tsx +61 -0
  106. package/src/inspector/index.tsx +2 -22
  107. package/src/inspector/pages/home.tsx +77 -0
  108. package/src/inspector/router/context.ts +21 -0
  109. package/src/inspector/router/hash-router.tsx +128 -0
  110. package/src/inspector/{viewer/use-page-path.ts → router/in-memory-router.tsx} +31 -29
  111. package/src/inspector/router/index.ts +4 -0
  112. package/src/inspector/standalone.tsx +60 -0
  113. package/src/inspector/tests/router/hash-router.test.tsx +847 -0
  114. package/src/inspector/tests/router/in-memory-router.test.tsx +724 -0
  115. package/src/inspector/ui/modal.tsx +5 -2
  116. package/src/inspector/viewer/breadcrumbs.tsx +5 -11
  117. package/src/inspector/viewer/header.tsx +67 -0
  118. package/src/inspector/viewer/page-stack.tsx +18 -26
  119. package/src/inspector/viewer/page.tsx +0 -1
  120. package/src/react/hooks.tsx +2 -0
  121. package/src/react/index.ts +1 -14
  122. package/src/react-core/hooks.ts +167 -18
  123. package/src/react-core/tests/createCoValueSubscriptionContext.test.tsx +18 -8
  124. package/src/react-core/tests/testUtils.tsx +67 -5
  125. package/src/react-core/tests/useCoState.test.ts +3 -7
  126. package/src/react-core/tests/useSubscriptionSelector.test.ts +3 -7
  127. package/src/react-core/tests/useSuspenseAccount.test.tsx +343 -0
  128. package/src/react-core/tests/useSuspenseCoState.test.tsx +1182 -0
  129. package/src/react-core/use.ts +46 -0
  130. package/src/react-native-core/crypto/RNCrypto.ts +1 -0
  131. package/src/react-native-core/hooks.tsx +2 -0
  132. package/src/react-native-core/platform.ts +2 -1
  133. package/src/tools/coValues/account.ts +13 -2
  134. package/src/tools/coValues/interfaces.ts +2 -3
  135. package/src/tools/implementation/ContextManager.ts +13 -0
  136. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +8 -1
  137. package/src/tools/subscribe/CoValueCoreSubscription.ts +71 -100
  138. package/src/tools/subscribe/SubscriptionCache.ts +272 -0
  139. package/src/tools/subscribe/SubscriptionScope.ts +113 -7
  140. package/src/tools/subscribe/utils.ts +77 -0
  141. package/src/tools/testing.ts +0 -3
  142. package/src/tools/tests/CoValueCoreSubscription.test.ts +46 -12
  143. package/src/tools/tests/ContextManager.test.ts +85 -0
  144. package/src/tools/tests/SubscriptionCache.test.ts +237 -0
  145. package/src/tools/tests/account.test.ts +11 -4
  146. package/src/tools/tests/coMap.test.ts +5 -7
  147. package/src/tools/tests/schema.resolved.test.ts +3 -3
  148. package/tsup.config.ts +2 -0
  149. package/dist/chunk-2S3Z2CN6.js.map +0 -1
  150. package/dist/inspector/custom-element-P76EIWEV.js.map +0 -1
  151. package/dist/inspector/viewer/new-app.d.ts.map +0 -1
  152. package/dist/inspector/viewer/use-page-path.d.ts +0 -10
  153. package/dist/inspector/viewer/use-page-path.d.ts.map +0 -1
  154. package/src/inspector/viewer/new-app.tsx +0 -156
@@ -15,14 +15,15 @@ interface ModalProps {
15
15
  onCancel?: () => void;
16
16
  showButtons?: boolean;
17
17
  className?: string;
18
+ wide?: boolean;
18
19
  }
19
20
 
20
- const ModalContent = styled("dialog")`
21
+ const ModalContent = styled("dialog")<{ wide?: boolean }>`
21
22
  background-color: var(--j-background);
22
23
  border-radius: var(--j-radius-lg);
23
24
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
24
25
  border: 1px solid var(--j-border-color);
25
- max-width: 32rem;
26
+ ${(props) => (props.wide ? "max-width: 60vw;" : "max-width: 32rem;")}
26
27
  margin-block: auto;
27
28
  margin-inline: auto;
28
29
  &::backdrop {
@@ -90,6 +91,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
90
91
  onCancel,
91
92
  showButtons = true,
92
93
  className,
94
+ wide = false,
93
95
  },
94
96
  ref,
95
97
  ) => {
@@ -123,6 +125,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
123
125
  role="dialog"
124
126
  aria-labelledby="modal-heading"
125
127
  onClose={onClose}
128
+ wide={wide}
126
129
  >
127
130
  <ModalHeader>
128
131
  <Heading id="modal-heading">{heading}</Heading>
@@ -1,7 +1,7 @@
1
1
  import { styled } from "goober";
2
2
  import React from "react";
3
3
  import { Button } from "../ui/button.js";
4
- import { PageInfo } from "./types.js";
4
+ import { useRouter } from "../router/context.js";
5
5
 
6
6
  const BreadcrumbsContainer = styled("div")`
7
7
  position: relative;
@@ -15,21 +15,15 @@ const Separator = styled("span")`
15
15
  padding: 0 0.125rem;
16
16
  `;
17
17
 
18
- interface BreadcrumbsProps {
19
- path: PageInfo[];
20
- onBreadcrumbClick: (index: number) => void;
21
- }
18
+ export const Breadcrumbs: React.FC<{}> = () => {
19
+ const { path, goToIndex } = useRouter();
22
20
 
23
- export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
24
- path,
25
- onBreadcrumbClick,
26
- }) => {
27
21
  return (
28
22
  <BreadcrumbsContainer>
29
23
  <Button
30
24
  variant="link"
31
25
  style={{ padding: "0 0.25rem" }}
32
- onClick={() => onBreadcrumbClick(-1)}
26
+ onClick={() => goToIndex(-1)}
33
27
  >
34
28
  Home
35
29
  </Button>
@@ -40,7 +34,7 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
40
34
  <Button
41
35
  variant="link"
42
36
  style={{ padding: "0 0.25rem" }}
43
- onClick={() => onBreadcrumbClick(index)}
37
+ onClick={() => goToIndex(index)}
44
38
  >
45
39
  {index === 0 ? page.name || "Root" : page.name}
46
40
  </Button>
@@ -0,0 +1,67 @@
1
+ import { CoID, RawCoValue } from "cojson";
2
+ import { styled } from "goober";
3
+ import React, { type PropsWithChildren, useState } from "react";
4
+ import { Button } from "../ui/button.js";
5
+ import { Input } from "../ui/input.js";
6
+ import { Breadcrumbs } from "./breadcrumbs.js";
7
+ import { DeleteLocalData } from "./delete-local-data.js";
8
+ import { useRouter } from "../router/context.js";
9
+
10
+ export function Header({
11
+ showDeleteLocalData = false,
12
+ showClose = false,
13
+ onClose,
14
+ children,
15
+ }: PropsWithChildren<{
16
+ showDeleteLocalData?: boolean;
17
+ showClose?: boolean;
18
+ onClose?: () => void;
19
+ }>) {
20
+ const [coValueId, setCoValueId] = useState<CoID<RawCoValue> | "">("");
21
+ const { path, setPage } = useRouter();
22
+
23
+ const handleCoValueIdSubmit = (e: React.FormEvent) => {
24
+ e.preventDefault();
25
+ if (coValueId) {
26
+ setPage(coValueId);
27
+ }
28
+ setCoValueId("");
29
+ };
30
+
31
+ return (
32
+ <HeaderContainer>
33
+ <Breadcrumbs />
34
+ {path.length !== 0 && (
35
+ <Form onSubmit={handleCoValueIdSubmit}>
36
+ <Input
37
+ label="CoValue ID"
38
+ style={{ fontFamily: "monospace" }}
39
+ hideLabel
40
+ placeholder="co_z1234567890abcdef123456789"
41
+ value={coValueId}
42
+ onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
43
+ />
44
+ </Form>
45
+ )}
46
+ {children}
47
+ {showDeleteLocalData && <DeleteLocalData />}
48
+ {showClose && (
49
+ <Button variant="plain" type="button" onClick={onClose}>
50
+ Close
51
+ </Button>
52
+ )}
53
+ </HeaderContainer>
54
+ );
55
+ }
56
+
57
+ const HeaderContainer = styled("div")`
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 1rem;
61
+ padding: 0 0.75rem;
62
+ margin: 0.75rem 0;
63
+ `;
64
+
65
+ const Form = styled("form")`
66
+ width: 24rem;
67
+ `;
@@ -2,23 +2,11 @@ import { CoID, LocalNode, RawCoValue } from "cojson";
2
2
  import { styled } from "goober";
3
3
  import { Page } from "./page.js";
4
4
  import { ErrorBoundary } from "../ui/error-boundary.js";
5
+ import { useRouter } from "../router/context.js";
6
+ import { useNode } from "../contexts/node.js";
7
+ import { HomePage } from "../pages/home.js";
5
8
 
6
- // Define the structure of a page in the path
7
- interface PageInfo {
8
- coId: CoID<RawCoValue>;
9
- name?: string;
10
- }
11
-
12
- // Props for the PageStack component
13
- interface PageStackProps {
14
- path: PageInfo[];
15
- node?: LocalNode | null;
16
- goBack: () => void;
17
- addPages: (pages: PageInfo[]) => void;
18
- children?: React.ReactNode;
19
- }
20
-
21
- const PageStackContainer = styled("div")`
9
+ const PageStackContainer = styled("article")`
22
10
  position: relative;
23
11
  padding: 0 0.75rem;
24
12
  overflow-y: auto;
@@ -27,25 +15,29 @@ const PageStackContainer = styled("div")`
27
15
  font-size: 16px;
28
16
  `;
29
17
 
30
- export function PageStack({
31
- path,
32
- node,
33
- goBack,
34
- addPages,
35
- children,
36
- }: PageStackProps) {
18
+ type PageStackProps = {
19
+ homePage?: React.ReactNode;
20
+ };
21
+
22
+ export function PageStack({ homePage }: PageStackProps) {
23
+ const { path, addPages, goBack } = useRouter();
24
+ const { localNode } = useNode();
25
+
37
26
  const page = path[path.length - 1];
38
27
  const index = path.length - 1;
39
28
 
29
+ if (path.length <= 0) {
30
+ return <PageStackContainer>{homePage ?? <HomePage />}</PageStackContainer>;
31
+ }
32
+
40
33
  return (
41
34
  <>
42
35
  <PageStackContainer>
43
- {children}
44
- {node && page && (
36
+ {localNode && page && (
45
37
  <ErrorBoundary title="An error occurred while rendering this CoValue">
46
38
  <Page
47
39
  coId={page.coId}
48
- node={node}
40
+ node={localNode}
49
41
  name={page.name || page.coId}
50
42
  onHeaderClick={goBack}
51
43
  onNavigate={addPages}
@@ -6,7 +6,6 @@ import {
6
6
  RawCoPlainText,
7
7
  RawCoStream,
8
8
  RawCoValue,
9
- RawGroup,
10
9
  } from "cojson";
11
10
  import { styled } from "goober";
12
11
  import React from "react";
@@ -56,4 +56,6 @@ export {
56
56
  useCoValueSubscription,
57
57
  useAccountSubscription,
58
58
  useSubscriptionSelector,
59
+ useSuspenseAccount,
60
+ useSuspenseCoState,
59
61
  } from "jazz-tools/react-core";
@@ -1,19 +1,6 @@
1
1
  export { JazzReactProvider } from "./provider.js";
2
2
  export type { JazzProviderProps } from "./provider.js";
3
- export {
4
- useAccount,
5
- useCoState,
6
- useAcceptInvite,
7
- experimental_useInboxSender,
8
- useJazzContext,
9
- useAuthSecretStorage,
10
- useAgent,
11
- useLogOut,
12
- useSyncConnectionStatus,
13
- useCoValueSubscription,
14
- useAccountSubscription,
15
- useSubscriptionSelector,
16
- } from "./hooks.js";
3
+ export * from "./hooks.js";
17
4
 
18
5
  export {
19
6
  createCoValueSubscriptionContext,
@@ -23,11 +23,11 @@ import {
23
23
  Loaded,
24
24
  MaybeLoaded,
25
25
  NotLoaded,
26
+ RefsToResolve,
26
27
  ResolveQuery,
27
28
  ResolveQueryStrict,
28
29
  SchemaResolveQuery,
29
30
  SubscriptionScope,
30
- coValueClassFromCoValueClassOrSchema,
31
31
  importContentPieces,
32
32
  captureStack,
33
33
  getUnloadedCoValueWithoutId,
@@ -36,6 +36,7 @@ import {
36
36
  import { JazzContext, JazzContextManagerContext } from "./provider.js";
37
37
  import { getCurrentAccountFromContextManager } from "./utils.js";
38
38
  import { CoValueSubscription } from "./types.js";
39
+ import { use } from "./use.js";
39
40
 
40
41
  export function useJazzContext<Acc extends Account>() {
41
42
  const value = useContext(JazzContext) as JazzContextType<Acc>;
@@ -134,20 +135,22 @@ export function useCoValueSubscription<
134
135
  const resolve = getResolveQuery(Schema, options?.resolve);
135
136
 
136
137
  const node = contextManager.getCurrentValue()!.node;
137
- const subscription = new SubscriptionScope<any>(
138
+ const cache = contextManager.getSubscriptionScopeCache();
139
+ const subscription = cache.getOrCreate(
138
140
  node,
139
- resolve,
141
+ Schema,
140
142
  id,
141
- {
142
- ref: coValueClassFromCoValueClassOrSchema(Schema),
143
- optional: true,
144
- },
143
+ resolve,
145
144
  false,
146
145
  false,
147
146
  options?.unstable_branch,
148
- callerStack.current,
149
147
  );
150
148
 
149
+ // Set callerStack on returned subscription after retrieval
150
+ if (callerStack.current) {
151
+ subscription.callerStack = callerStack.current;
152
+ }
153
+
151
154
  return {
152
155
  value: subscription,
153
156
  contextManager,
@@ -182,7 +185,6 @@ export function useCoValueSubscription<
182
185
  subscription.branchOwnerId !== branchOwnerId ||
183
186
  subscription.agent !== agent
184
187
  ) {
185
- subscription.value?.destroy();
186
188
  subscriptionRef.current = createSubscription();
187
189
  subscription = subscriptionRef.current;
188
190
  }
@@ -442,6 +444,79 @@ export function useCoState<
442
444
  return value;
443
445
  }
444
446
 
447
+ export function useSuspenseCoState<
448
+ S extends CoValueClassOrSchema,
449
+ // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
450
+ const R extends ResolveQuery<S> = SchemaResolveQuery<S>,
451
+ TSelectorReturn = Loaded<S, R>,
452
+ >(
453
+ /** The CoValue schema or class constructor */
454
+ Schema: S,
455
+ /** The ID of the CoValue to subscribe to */
456
+ id: string,
457
+ /** Optional configuration for the subscription */
458
+ options?: {
459
+ /** Resolve query to specify which nested CoValues to load */
460
+ resolve?: ResolveQueryStrict<S, R>;
461
+ /** Select which value to return */
462
+ select?: (value: Loaded<S, R>) => TSelectorReturn;
463
+ /** Equality function to determine if the selected value has changed, defaults to `Object.is` */
464
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
465
+ /**
466
+ * Create or load a branch for isolated editing.
467
+ *
468
+ * Branching lets you take a snapshot of the current state and start modifying it without affecting the canonical/shared version.
469
+ * It's a fork of your data graph: the same schema, but with diverging values.
470
+ *
471
+ * The checkout of the branch is applied on all the resolved values.
472
+ *
473
+ * @param name - A unique name for the branch. This identifies the branch
474
+ * and can be used to switch between different branches of the same CoValue.
475
+ * @param owner - The owner of the branch. Determines who can access and modify
476
+ * the branch. If not provided, the branch is owned by the current user.
477
+ *
478
+ * For more info see the [branching](https://jazz.tools/docs/react/using-covalues/version-control) documentation.
479
+ */
480
+ unstable_branch?: BranchDefinition;
481
+ preloaded?: ExportedCoValue<Loaded<S, R>>;
482
+ },
483
+ ): TSelectorReturn {
484
+ useImportCoValueContent(id, options?.preloaded);
485
+
486
+ const subscription = useCoValueSubscription(Schema, id, options);
487
+
488
+ if (!subscription) {
489
+ throw new Error("Subscription not found");
490
+ }
491
+
492
+ use(subscription.getPromise());
493
+
494
+ const getCurrentValue = () => {
495
+ const value = subscription.getCurrentValue();
496
+
497
+ if (!value.$isLoaded) {
498
+ throw new Error("CoValue must be loaded in a suspense context");
499
+ }
500
+
501
+ return value;
502
+ };
503
+
504
+ const value = useSyncExternalStoreWithSelector<Loaded<S, R>, TSelectorReturn>(
505
+ React.useCallback(
506
+ (callback) => {
507
+ return subscription.subscribe(callback);
508
+ },
509
+ [subscription],
510
+ ),
511
+ getCurrentValue,
512
+ getCurrentValue,
513
+ options?.select ?? ((value) => value as TSelectorReturn),
514
+ options?.equalityFn ?? Object.is,
515
+ );
516
+
517
+ return value;
518
+ }
519
+
445
520
  export function useSubscriptionSelector<
446
521
  S extends CoValueClassOrSchema,
447
522
  // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
@@ -510,20 +585,22 @@ export function useAccountSubscription<
510
585
  const resolve = getResolveQuery(Schema, options?.resolve);
511
586
 
512
587
  const node = contextManager.getCurrentValue()!.node;
513
- const subscription = new SubscriptionScope<any>(
588
+ const cache = contextManager.getSubscriptionScopeCache();
589
+ const subscription = cache.getOrCreate(
514
590
  node,
515
- resolve,
591
+ Schema,
516
592
  agent.$jazz.id,
517
- {
518
- ref: coValueClassFromCoValueClassOrSchema(Schema),
519
- optional: true,
520
- },
593
+ resolve,
521
594
  false,
522
595
  false,
523
596
  options?.unstable_branch,
524
- callerStack.current,
525
597
  );
526
598
 
599
+ // Set callerStack on returned subscription after retrieval
600
+ if (callerStack.current) {
601
+ subscription.callerStack = callerStack.current;
602
+ }
603
+
527
604
  return {
528
605
  subscription,
529
606
  contextManager,
@@ -545,12 +622,12 @@ export function useAccountSubscription<
545
622
  subscription.branchName !== options?.unstable_branch?.name ||
546
623
  subscription.branchOwnerId !== options?.unstable_branch?.owner?.$jazz.id
547
624
  ) {
548
- subscription.subscription?.destroy();
625
+ // No need to manually destroy - cache handles cleanup via SubscriptionScope lifecycle
549
626
  setSubscription(createSubscription());
550
627
  }
551
628
 
552
629
  return contextManager.subscribe(() => {
553
- subscription.subscription?.destroy();
630
+ // No need to manually destroy - cache handles cleanup via SubscriptionScope lifecycle
554
631
  setSubscription(createSubscription());
555
632
  });
556
633
  }, [Schema, contextManager, branchName, branchOwnerId]);
@@ -705,6 +782,78 @@ export function useAccount<
705
782
  );
706
783
  }
707
784
 
785
+ export function useSuspenseAccount<
786
+ A extends AccountClass<Account> | AnyAccountSchema,
787
+ // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
788
+ const R extends ResolveQuery<A> = SchemaResolveQuery<A>,
789
+ TSelectorReturn = Loaded<A, R>,
790
+ >(
791
+ /** The account schema to use. Defaults to the base Account schema */
792
+ AccountSchema: A = Account as unknown as A,
793
+ /** Optional configuration for the subscription */
794
+ options?: {
795
+ /** Resolve query to specify which nested CoValues to load from the account */
796
+ resolve?: ResolveQueryStrict<A, R>;
797
+ /** Select which value to return from the account data */
798
+ select?: (account: Loaded<A, R>) => TSelectorReturn;
799
+ /** Equality function to determine if the selected value has changed, defaults to `Object.is` */
800
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
801
+ /**
802
+ * Create or load a branch for isolated editing.
803
+ *
804
+ * Branching lets you take a snapshot of the current state and start modifying it without affecting the canonical/shared version.
805
+ * It's a fork of your data graph: the same schema, but with diverging values.
806
+ *
807
+ * The checkout of the branch is applied on all the resolved values.
808
+ *
809
+ * @param name - A unique name for the branch. This identifies the branch
810
+ * and can be used to switch between different branches of the same CoValue.
811
+ * @param owner - The owner of the branch. Determines who can access and modify
812
+ * the branch. If not provided, the branch is owned by the current user.
813
+ *
814
+ * For more info see the [branching](https://jazz.tools/docs/react/using-covalues/version-control) documentation.
815
+ */
816
+ unstable_branch?: BranchDefinition;
817
+ },
818
+ ): TSelectorReturn {
819
+ const subscription = useAccountSubscription(AccountSchema, options);
820
+
821
+ if (!subscription) {
822
+ throw new Error(
823
+ "Subscription not found, are you using useSuspenseAccount in guest mode?",
824
+ );
825
+ }
826
+
827
+ use(subscription.getPromise());
828
+
829
+ const getCurrentValue = () => {
830
+ const value = subscription.getCurrentValue();
831
+
832
+ if (!value.$isLoaded) {
833
+ throw new Error("Account must be loaded in a suspense context");
834
+ }
835
+
836
+ return value;
837
+ };
838
+
839
+ return useSyncExternalStoreWithSelector<Loaded<A, R>, TSelectorReturn>(
840
+ React.useCallback(
841
+ (callback) => {
842
+ if (!subscription) {
843
+ return () => {};
844
+ }
845
+
846
+ return subscription.subscribe(callback);
847
+ },
848
+ [subscription],
849
+ ),
850
+ getCurrentValue,
851
+ getCurrentValue,
852
+ options?.select ?? ((value) => value as TSelectorReturn),
853
+ options?.equalityFn ?? Object.is,
854
+ );
855
+ }
856
+
708
857
  /**
709
858
  * Returns a function for logging out of the current account.
710
859
  */
@@ -164,15 +164,31 @@ describe("createCoValueSubscriptionContext", () => {
164
164
  });
165
165
 
166
166
  it("the provider shows a loading fallback when loading the coValue", async () => {
167
+ await setupJazzTestSync({
168
+ asyncPeers: true,
169
+ });
170
+
167
171
  const TestMap = co.map({
168
172
  value: z.string(),
169
173
  });
170
174
 
175
+ const map = TestMap.create({
176
+ value: "123",
177
+ });
178
+
179
+ await createJazzTestAccount({
180
+ isCurrentActiveAccount: true,
181
+ });
182
+
171
183
  const { Provider } = createCoValueSubscriptionContext(TestMap);
172
184
 
173
185
  const { container } = render(
174
186
  <JazzTestProvider>
175
- <Provider id="co_test123" loadingFallback={<div>Loading...</div>}>
187
+ <Provider
188
+ id={map.$jazz.id}
189
+ loadingFallback={<div>Loading...</div>}
190
+ unavailableFallback={<div>Unavailable</div>}
191
+ >
176
192
  <div>Children should not render</div>
177
193
  </Provider>
178
194
  </JazzTestProvider>,
@@ -203,13 +219,7 @@ describe("createCoValueSubscriptionContext", () => {
203
219
  </JazzTestProvider>,
204
220
  );
205
221
 
206
- // Initially shows loading fallback
207
- expect(container.textContent).toContain("Loading...");
208
-
209
- // Should show unavailable fallback after CoValue load timeout
210
- await waitFor(() => {
211
- expect(container.textContent).toContain("Unavailable");
212
- });
222
+ expect(container.textContent).toContain("Unavailable");
213
223
 
214
224
  // Children should never be rendered
215
225
  expect(container.textContent).not.toContain("Children should not render");
@@ -4,9 +4,12 @@ import {
4
4
  render,
5
5
  renderHook,
6
6
  } from "@testing-library/react";
7
- import { Account, AnonymousJazzAgent, AuthSecretStorage } from "jazz-tools";
7
+ import { Account, AnonymousJazzAgent } from "jazz-tools";
8
8
  import React from "react";
9
9
  import { JazzTestProvider } from "../testing.js";
10
+ import { getSqliteStorageAsync, SQLiteDatabaseDriverAsync } from "cojson";
11
+ import Database, { type Database as DatabaseT } from "libsql";
12
+ import { onTestFinished } from "vitest";
10
13
 
11
14
  type JazzExtendedOptions = {
12
15
  account?: Account | { guest: AnonymousJazzAgent };
@@ -23,12 +26,16 @@ const customRender = (
23
26
  account={options.account}
24
27
  isAuthenticated={options.isAuthenticated}
25
28
  >
26
- {children}
29
+ {options.wrapper ? (
30
+ <options.wrapper>{children}</options.wrapper>
31
+ ) : (
32
+ children
33
+ )}
27
34
  </JazzTestProvider>
28
35
  );
29
36
  };
30
37
 
31
- return render(ui, { wrapper: AllTheProviders, ...options });
38
+ return render(ui, { ...options, wrapper: AllTheProviders });
32
39
  };
33
40
 
34
41
  const customRenderHook = <TProps, TResult>(
@@ -41,12 +48,16 @@ const customRenderHook = <TProps, TResult>(
41
48
  account={options.account}
42
49
  isAuthenticated={options.isAuthenticated}
43
50
  >
44
- {children}
51
+ {options.wrapper ? (
52
+ <options.wrapper>{children}</options.wrapper>
53
+ ) : (
54
+ children
55
+ )}
45
56
  </JazzTestProvider>
46
57
  );
47
58
  };
48
59
 
49
- return renderHook(callback, { wrapper: AllTheProviders, ...options });
60
+ return renderHook(callback, { ...options, wrapper: AllTheProviders });
50
61
  };
51
62
 
52
63
  // re-export everything
@@ -55,3 +66,54 @@ export * from "@testing-library/react";
55
66
  // override render method
56
67
  export { customRender as render };
57
68
  export { customRenderHook as renderHook };
69
+
70
+ class LibSQLSqliteAsyncDriver implements SQLiteDatabaseDriverAsync {
71
+ private readonly db: DatabaseT;
72
+
73
+ constructor(filename: string) {
74
+ this.db = new Database(filename, {});
75
+ }
76
+
77
+ async initialize() {
78
+ await this.db.pragma("journal_mode = WAL");
79
+ }
80
+
81
+ async run(sql: string, params: unknown[]) {
82
+ this.db.prepare(sql).run(params);
83
+ }
84
+
85
+ async query<T>(sql: string, params: unknown[]): Promise<T[]> {
86
+ return this.db.prepare(sql).all(params) as T[];
87
+ }
88
+
89
+ async get<T>(sql: string, params: unknown[]): Promise<T | undefined> {
90
+ return this.db.prepare(sql).get(params) as T | undefined;
91
+ }
92
+
93
+ async transaction(callback: () => unknown) {
94
+ await this.run("BEGIN TRANSACTION", []);
95
+
96
+ try {
97
+ await callback();
98
+ await this.run("COMMIT", []);
99
+ } catch (error) {
100
+ await this.run("ROLLBACK", []);
101
+ }
102
+ }
103
+
104
+ async closeDb() {
105
+ this.db.close();
106
+ }
107
+ }
108
+
109
+ export async function createAsyncStorage() {
110
+ const storage = await getSqliteStorageAsync(
111
+ new LibSQLSqliteAsyncDriver(":memory:"),
112
+ );
113
+
114
+ onTestFinished(() => {
115
+ storage.close();
116
+ });
117
+
118
+ return storage;
119
+ }
@@ -64,13 +64,9 @@ describe("useCoState", () => {
64
64
  account,
65
65
  });
66
66
 
67
- expect(result.current.$jazz.loadingState).toBe(CoValueLoadingState.LOADING);
68
-
69
- await waitFor(() => {
70
- expect(result.current.$jazz.loadingState).toBe(
71
- CoValueLoadingState.UNAVAILABLE,
72
- );
73
- });
67
+ expect(result.current.$jazz.loadingState).toBe(
68
+ CoValueLoadingState.UNAVAILABLE,
69
+ );
74
70
  });
75
71
 
76
72
  it("should update the value when the coValue changes", async () => {