jazz-tools 0.19.10 → 0.19.12

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 (112) hide show
  1. package/.turbo/turbo-build.log +58 -54
  2. package/CHANGELOG.md +23 -0
  3. package/dist/{chunk-FFEEPZEG.js → chunk-AGF4HEDH.js} +61 -28
  4. package/dist/chunk-AGF4HEDH.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-YQNK5Y7B.js +4108 -0
  9. package/dist/inspector/chunk-YQNK5Y7B.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-KYV64IOC.js} +1057 -918
  13. package/dist/inspector/custom-element-KYV64IOC.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/tests/utils/transactions-changes.test.d.ts +2 -0
  40. package/dist/inspector/tests/utils/transactions-changes.test.d.ts.map +1 -0
  41. package/dist/inspector/ui/modal.d.ts +1 -0
  42. package/dist/inspector/ui/modal.d.ts.map +1 -1
  43. package/dist/inspector/utils/transactions-changes.d.ts +13 -13
  44. package/dist/inspector/utils/transactions-changes.d.ts.map +1 -1
  45. package/dist/inspector/viewer/breadcrumbs.d.ts +1 -7
  46. package/dist/inspector/viewer/breadcrumbs.d.ts.map +1 -1
  47. package/dist/inspector/viewer/header.d.ts +7 -0
  48. package/dist/inspector/viewer/header.d.ts.map +1 -0
  49. package/dist/inspector/viewer/page-stack.d.ts +4 -13
  50. package/dist/inspector/viewer/page-stack.d.ts.map +1 -1
  51. package/dist/inspector/viewer/page.d.ts.map +1 -1
  52. package/dist/react/index.js +4 -1
  53. package/dist/react/index.js.map +1 -1
  54. package/dist/react/provider.d.ts.map +1 -1
  55. package/dist/react-core/index.js +2 -2
  56. package/dist/react-core/index.js.map +1 -1
  57. package/dist/react-native/index.js +4 -1
  58. package/dist/react-native/index.js.map +1 -1
  59. package/dist/react-native-core/index.js +4 -1
  60. package/dist/react-native-core/index.js.map +1 -1
  61. package/dist/react-native-core/provider.d.ts.map +1 -1
  62. package/dist/testing.js +1 -1
  63. package/dist/tools/coValues/account.d.ts +7 -1
  64. package/dist/tools/coValues/account.d.ts.map +1 -1
  65. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  66. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +8 -1
  67. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  68. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  69. package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -6
  70. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  71. package/dist/tools/testing.d.ts.map +1 -1
  72. package/package.json +9 -4
  73. package/src/inspector/account-switcher.tsx +440 -0
  74. package/src/inspector/contexts/node.tsx +129 -0
  75. package/src/inspector/custom-element.tsx +2 -2
  76. package/src/inspector/in-app.tsx +61 -0
  77. package/src/inspector/index.tsx +2 -22
  78. package/src/inspector/pages/home.tsx +77 -0
  79. package/src/inspector/router/context.ts +21 -0
  80. package/src/inspector/router/hash-router.tsx +128 -0
  81. package/src/inspector/{viewer/use-page-path.ts → router/in-memory-router.tsx} +31 -29
  82. package/src/inspector/router/index.ts +4 -0
  83. package/src/inspector/standalone.tsx +60 -0
  84. package/src/inspector/tests/router/hash-router.test.tsx +847 -0
  85. package/src/inspector/tests/router/in-memory-router.test.tsx +724 -0
  86. package/src/inspector/tests/utils/transactions-changes.test.ts +102 -0
  87. package/src/inspector/ui/icons/add-icon.tsx +3 -3
  88. package/src/inspector/ui/modal.tsx +5 -2
  89. package/src/inspector/utils/history.ts +6 -6
  90. package/src/inspector/utils/transactions-changes.ts +37 -3
  91. package/src/inspector/viewer/breadcrumbs.tsx +5 -11
  92. package/src/inspector/viewer/header.tsx +67 -0
  93. package/src/inspector/viewer/history-view.tsx +13 -13
  94. package/src/inspector/viewer/page-stack.tsx +18 -26
  95. package/src/inspector/viewer/page.tsx +0 -1
  96. package/src/react/provider.tsx +6 -1
  97. package/src/react-core/hooks.ts +2 -2
  98. package/src/react-core/tests/useSuspenseCoState.test.tsx +47 -0
  99. package/src/react-native-core/provider.tsx +6 -1
  100. package/src/tools/coValues/account.ts +13 -2
  101. package/src/tools/implementation/ContextManager.ts +10 -0
  102. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +8 -1
  103. package/src/tools/subscribe/SubscriptionScope.ts +61 -39
  104. package/src/tools/tests/account.test.ts +11 -4
  105. package/src/tools/tests/schema.resolved.test.ts +3 -3
  106. package/tsup.config.ts +1 -0
  107. package/dist/chunk-FFEEPZEG.js.map +0 -1
  108. package/dist/inspector/custom-element-P76EIWEV.js.map +0 -1
  109. package/dist/inspector/viewer/new-app.d.ts.map +0 -1
  110. package/dist/inspector/viewer/use-page-path.d.ts +0 -10
  111. package/dist/inspector/viewer/use-page-path.d.ts.map +0 -1
  112. package/src/inspector/viewer/new-app.tsx +0 -156
@@ -0,0 +1,102 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { setActiveAccount, setupJazzTestSync } from "jazz-tools/testing";
3
+ import { co, z } from "jazz-tools";
4
+ import * as TransactionsChanges from "../../utils/transactions-changes";
5
+
6
+ describe("transactions changes", async () => {
7
+ const account = await setupJazzTestSync();
8
+ setActiveAccount(account);
9
+
10
+ describe("ambiguous values in Group's transactions", () => {
11
+ it("isGroupExtension should return false for a CoMap", () => {
12
+ const value = co.map({ test: z.string() }).create({ test: "extend" })
13
+ .$jazz.raw;
14
+
15
+ const transactions = value.core.verifiedTransactions;
16
+ expect(
17
+ TransactionsChanges.isGroupExtension(
18
+ value,
19
+ transactions[0]?.changes?.[0],
20
+ ),
21
+ ).toBe(false);
22
+ });
23
+
24
+ it("isGroupExtendRevocation should return false for a CoMap", () => {
25
+ const value = co.map({ test: z.string() }).create({ test: "revoked" })
26
+ .$jazz.raw;
27
+
28
+ const transactions = value.core.verifiedTransactions;
29
+ expect(
30
+ TransactionsChanges.isGroupExtendRevocation(
31
+ value,
32
+ transactions[0]?.changes?.[0],
33
+ ),
34
+ ).toBe(false);
35
+ });
36
+
37
+ it("isGroupPromotion should return false for a CoMap", () => {
38
+ const value = co
39
+ .map({ parent_co_test: z.string() })
40
+ .create({ parent_co_test: "foo" }).$jazz.raw;
41
+
42
+ const transactions = value.core.verifiedTransactions;
43
+ expect(
44
+ TransactionsChanges.isGroupPromotion(
45
+ value,
46
+ transactions[0]?.changes?.[0],
47
+ ),
48
+ ).toBe(false);
49
+ });
50
+
51
+ it("isUserPromotion should return false for a CoMap", () => {
52
+ const value = co.map({ everyone: z.string() }).create({ everyone: "foo" })
53
+ .$jazz.raw;
54
+
55
+ const transactions = value.core.verifiedTransactions;
56
+ expect(
57
+ TransactionsChanges.isUserPromotion(
58
+ value,
59
+ transactions[0]?.changes?.[0],
60
+ ),
61
+ ).toBe(false);
62
+ });
63
+
64
+ it("isUserPromotion should return false for a CoMap", () => {
65
+ const value = co.map({ everyone: z.string() }).create({ everyone: "foo" })
66
+ .$jazz.raw;
67
+
68
+ const transactions = value.core.verifiedTransactions;
69
+ expect(
70
+ TransactionsChanges.isUserPromotion(
71
+ value,
72
+ transactions[0]?.changes?.[0],
73
+ ),
74
+ ).toBe(false);
75
+
76
+ const value2 = co.map({ co_z123: z.string() }).create({ co_z123: "foo" })
77
+ .$jazz.raw;
78
+
79
+ const transactions2 = value2.core.verifiedTransactions;
80
+ expect(
81
+ TransactionsChanges.isUserPromotion(
82
+ value2,
83
+ transactions2[0]?.changes?.[0],
84
+ ),
85
+ ).toBe(false);
86
+ });
87
+
88
+ it("isKeyRevelation should return false for a CoMap", () => {
89
+ const value = co
90
+ .map({ "123_for_test": z.string() })
91
+ .create({ "123_for_test": "foo" }).$jazz.raw;
92
+
93
+ const transactions = value.core.verifiedTransactions;
94
+ expect(
95
+ TransactionsChanges.isKeyRevelation(
96
+ value,
97
+ transactions[0]?.changes?.[0],
98
+ ),
99
+ ).toBe(false);
100
+ });
101
+ });
102
+ });
@@ -12,9 +12,9 @@ export function AddIcon(props: React.SVGProps<SVGSVGElement>) {
12
12
  >
13
13
  <path
14
14
  d="M4 12H20M12 4V20"
15
- stroke-width="2"
16
- stroke-linecap="round"
17
- stroke-linejoin="round"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
18
  />
19
19
  </svg>
20
20
  );
@@ -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>
@@ -62,10 +62,10 @@ export function getTransactionChanges(
62
62
  const firstChange = tx.changes[0]!;
63
63
 
64
64
  if (
65
- TransactionChanges.isItemAppend(firstChange) &&
65
+ TransactionChanges.isItemAppend(coValue, firstChange) &&
66
66
  tx.changes.every(
67
67
  (c) =>
68
- TransactionChanges.isItemAppend(c) &&
68
+ TransactionChanges.isItemAppend(coValue, c) &&
69
69
  areSameOpIds(c.after, firstChange.after),
70
70
  )
71
71
  ) {
@@ -84,10 +84,10 @@ export function getTransactionChanges(
84
84
  }
85
85
 
86
86
  if (
87
- TransactionChanges.isItemPrepend(firstChange) &&
87
+ TransactionChanges.isItemPrepend(coValue, firstChange) &&
88
88
  tx.changes.every(
89
89
  (c) =>
90
- TransactionChanges.isItemPrepend(c) &&
90
+ TransactionChanges.isItemPrepend(coValue, c) &&
91
91
  areSameOpIds(c.before, firstChange.before),
92
92
  )
93
93
  ) {
@@ -106,8 +106,8 @@ export function getTransactionChanges(
106
106
  }
107
107
 
108
108
  if (
109
- TransactionChanges.isItemDeletion(firstChange) &&
110
- tx.changes.every((c) => TransactionChanges.isItemDeletion(c))
109
+ TransactionChanges.isItemDeletion(coValue, firstChange) &&
110
+ tx.changes.every((c) => TransactionChanges.isItemDeletion(coValue, c))
111
111
  ) {
112
112
  const coValueBeforeDeletions = coValue.atTime(tx.madeAt - 1);
113
113
 
@@ -14,85 +14,119 @@ import type {
14
14
  import { isCoId } from "../viewer/types";
15
15
 
16
16
  export const isGroupExtension = (
17
+ coValue: RawCoValue,
17
18
  change: any,
18
19
  ): change is Extract<
19
20
  MapOpPayload<`child_${string}`, "extend">,
20
21
  { op: "set" }
21
22
  > => {
23
+ if (coValue.core.isGroup() === false) return false;
22
24
  return change?.op === "set" && change?.value === "extend";
23
25
  };
24
26
 
25
27
  export const isGroupExtendRevocation = (
28
+ coValue: RawCoValue,
26
29
  change: any,
27
30
  ): change is Extract<
28
31
  MapOpPayload<`child_${string}`, "revoked">,
29
32
  { op: "set" }
30
33
  > => {
34
+ if (coValue.core.isGroup() === false) return false;
31
35
  return change?.op === "set" && change?.value === "revoked";
32
36
  };
33
37
 
34
38
  export const isGroupPromotion = (
39
+ coValue: RawCoValue,
35
40
  change: any,
36
41
  ): change is Extract<
37
42
  MapOpPayload<`parent_co_${string}`, AccountRole>,
38
43
  { op: "set" }
39
44
  > => {
45
+ if (coValue.core.isGroup() === false) return false;
40
46
  return change?.op === "set" && change?.key.startsWith("parent_co_");
41
47
  };
42
48
 
43
49
  export const isUserPromotion = (
50
+ coValue: RawCoValue,
44
51
  change: any,
45
52
  ): change is Extract<MapOpPayload<CoID<RawCoValue>, Role>, { op: "set" }> => {
53
+ if (coValue.core.isGroup() === false) return false;
46
54
  return (
47
55
  change?.op === "set" && (isCoId(change?.key) || change?.key === "everyone")
48
56
  );
49
57
  };
50
58
 
51
59
  export const isKeyRevelation = (
60
+ coValue: RawCoValue,
52
61
  change: any,
53
62
  ): change is Extract<
54
63
  MapOpPayload<`${string}_for_${string}`, string>,
55
64
  { op: "set" }
56
65
  > => {
66
+ if (
67
+ coValue.core.isGroup() === false &&
68
+ coValue.headerMeta?.type !== "account"
69
+ )
70
+ return false;
57
71
  return change?.op === "set" && change?.key.includes("_for_");
58
72
  };
59
73
 
60
74
  export const isPropertySet = (
75
+ coValue: RawCoValue,
61
76
  change: any,
62
77
  ): change is Extract<MapOpPayload<string, any>, { op: "set" }> => {
63
78
  return change?.op === "set" && "key" in change && "value" in change;
64
79
  };
65
80
  export const isPropertyDeletion = (
81
+ coValue: RawCoValue,
66
82
  change: any,
67
83
  ): change is Extract<MapOpPayload<string, any>, { op: "del" }> => {
68
84
  return change?.op === "del" && "key" in change;
69
85
  };
70
86
 
71
87
  export const isItemAppend = (
88
+ coValue: RawCoValue,
72
89
  change: any,
73
90
  ): change is Extract<ListOpPayload<any>, { op: "app" }> => {
91
+ if (coValue.type !== "colist" && coValue.type !== "coplaintext") return false;
74
92
  return change?.op === "app" && "after" in change && "value" in change;
75
93
  };
76
94
  export const isItemPrepend = (
95
+ coValue: RawCoValue,
77
96
  change: any,
78
97
  ): change is Extract<ListOpPayload<any>, { op: "pre" }> => {
98
+ if (coValue.type !== "colist" && coValue.type !== "coplaintext") return false;
79
99
  return change?.op === "pre" && "before" in change && "value" in change;
80
100
  };
81
101
 
82
102
  export const isItemDeletion = (
103
+ coValue: RawCoValue,
83
104
  change: any,
84
105
  ): change is Extract<ListOpPayload<any>, { op: "del" }> => {
106
+ if (coValue.type !== "colist" && coValue.type !== "coplaintext") return false;
85
107
  return change?.op === "del" && "insertion" in change;
86
108
  };
87
109
 
88
- export const isStreamStart = (change: any): change is BinaryStreamStart => {
110
+ export const isStreamStart = (
111
+ coValue: RawCoValue,
112
+ change: any,
113
+ ): change is BinaryStreamStart => {
114
+ if (coValue.type !== "coStream") return false;
89
115
  return change?.type === "start" && "mimeType" in change;
90
116
  };
91
117
 
92
- export const isStreamChunk = (change: any): change is BinaryStreamChunk => {
118
+ export const isStreamChunk = (
119
+ coValue: RawCoValue,
120
+ change: any,
121
+ ): change is BinaryStreamChunk => {
122
+ if (coValue.type !== "coStream") return false;
93
123
  return change?.type === "chunk" && "chunk" in change;
94
124
  };
95
125
 
96
- export const isStreamEnd = (change: any): change is BinaryStreamEnd => {
126
+ export const isStreamEnd = (
127
+ coValue: RawCoValue,
128
+ change: any,
129
+ ): change is BinaryStreamEnd => {
130
+ if (coValue.type !== "coStream") return false;
97
131
  return change?.type === "end";
98
132
  };
@@ -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
+ `;
@@ -128,7 +128,7 @@ function mapTransactionToAction(
128
128
  coValue: RawCoValue,
129
129
  ): string {
130
130
  // Group changes
131
- if (TransactionChanges.isUserPromotion(change)) {
131
+ if (TransactionChanges.isUserPromotion(coValue, change)) {
132
132
  if (change.value === "revoked") {
133
133
  return `${change.key} has been revoked`;
134
134
  }
@@ -136,28 +136,28 @@ function mapTransactionToAction(
136
136
  return `${change.key} has been promoted to ${change.value}`;
137
137
  }
138
138
 
139
- if (TransactionChanges.isGroupExtension(change)) {
139
+ if (TransactionChanges.isGroupExtension(coValue, change)) {
140
140
  const child = change.key.slice(6);
141
141
  return `Group became a member of ${child}`;
142
142
  }
143
143
 
144
- if (TransactionChanges.isGroupExtendRevocation(change)) {
144
+ if (TransactionChanges.isGroupExtendRevocation(coValue, change)) {
145
145
  const child = change.key.slice(6);
146
146
  return `Group's membership of ${child} has been revoked.`;
147
147
  }
148
148
 
149
- if (TransactionChanges.isGroupPromotion(change)) {
149
+ if (TransactionChanges.isGroupPromotion(coValue, change)) {
150
150
  const parent = change.key.slice(7);
151
151
  return `Group ${parent} has been promoted to ${change.value}`;
152
152
  }
153
153
 
154
- if (TransactionChanges.isKeyRevelation(change)) {
154
+ if (TransactionChanges.isKeyRevelation(coValue, change)) {
155
155
  const [key, target] = change.key.split("_for_");
156
156
  return `Key "${key}" has been revealed to "${target}"`;
157
157
  }
158
158
 
159
159
  // coList changes
160
- if (TransactionChanges.isItemAppend(change)) {
160
+ if (TransactionChanges.isItemAppend(coValue, change)) {
161
161
  if (change.after === "start") {
162
162
  return `"${change.value}" has been appended`;
163
163
  }
@@ -171,7 +171,7 @@ function mapTransactionToAction(
171
171
  return `"${change.value}" has been inserted after "${(after as any).value}"`;
172
172
  }
173
173
 
174
- if (TransactionChanges.isItemPrepend(change)) {
174
+ if (TransactionChanges.isItemPrepend(coValue, change)) {
175
175
  if (change.before === "end") {
176
176
  return `"${change.value}" has been prepended`;
177
177
  }
@@ -185,7 +185,7 @@ function mapTransactionToAction(
185
185
  return `"${change.value}" has been inserted before "${(before as any).value}"`;
186
186
  }
187
187
 
188
- if (TransactionChanges.isItemDeletion(change)) {
188
+ if (TransactionChanges.isItemDeletion(coValue, change)) {
189
189
  const insertion = findListChange(change.insertion, coValue);
190
190
  if (insertion === undefined) {
191
191
  return `An undefined item has been deleted`;
@@ -195,24 +195,24 @@ function mapTransactionToAction(
195
195
  }
196
196
 
197
197
  // coStream changes
198
- if (TransactionChanges.isStreamStart(change)) {
198
+ if (TransactionChanges.isStreamStart(coValue, change)) {
199
199
  return `Stream started with mime type "${change.mimeType}" and file name "${change.fileName}"`;
200
200
  }
201
201
 
202
- if (TransactionChanges.isStreamChunk(change)) {
202
+ if (TransactionChanges.isStreamChunk(coValue, change)) {
203
203
  return `Stream chunk added`;
204
204
  }
205
205
 
206
- if (TransactionChanges.isStreamEnd(change)) {
206
+ if (TransactionChanges.isStreamEnd(coValue, change)) {
207
207
  return `Stream ended`;
208
208
  }
209
209
 
210
210
  // coMap changes
211
- if (TransactionChanges.isPropertySet(change)) {
211
+ if (TransactionChanges.isPropertySet(coValue, change)) {
212
212
  return `Property "${change.key}" has been set to ${JSON.stringify(change.value)}`;
213
213
  }
214
214
 
215
- if (TransactionChanges.isPropertyDeletion(change)) {
215
+ if (TransactionChanges.isPropertyDeletion(coValue, change)) {
216
216
  return `Property "${change.key}" has been deleted`;
217
217
  }
218
218
 
@@ -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";
@@ -58,6 +58,9 @@ export function JazzReactProvider<
58
58
  );
59
59
  const logoutReplacementActiveRef = useRef(false);
60
60
  logoutReplacementActiveRef.current = Boolean(logOutReplacement);
61
+ const onAnonymousAccountDiscardedEnabled = Boolean(
62
+ onAnonymousAccountDiscarded,
63
+ );
61
64
 
62
65
  const value = React.useSyncExternalStore<
63
66
  JazzContextType<InstanceOfSchema<S>> | undefined
@@ -74,7 +77,9 @@ export function JazzReactProvider<
74
77
  logOutReplacement: logoutReplacementActiveRef.current
75
78
  ? logOutReplacementRefCallback
76
79
  : undefined,
77
- onAnonymousAccountDiscarded: onAnonymousAccountDiscardedRefCallback,
80
+ onAnonymousAccountDiscarded: onAnonymousAccountDiscardedEnabled
81
+ ? onAnonymousAccountDiscardedRefCallback
82
+ : undefined,
78
83
  } satisfies JazzContextManagerProps<S>;
79
84
 
80
85
  if (contextManager.propsChanged(props)) {
@@ -489,7 +489,7 @@ export function useSuspenseCoState<
489
489
  throw new Error("Subscription not found");
490
490
  }
491
491
 
492
- use(subscription.getPromise());
492
+ use(subscription.getCachedPromise());
493
493
 
494
494
  const getCurrentValue = () => {
495
495
  const value = subscription.getCurrentValue();
@@ -824,7 +824,7 @@ export function useSuspenseAccount<
824
824
  );
825
825
  }
826
826
 
827
- use(subscription.getPromise());
827
+ use(subscription.getCachedPromise());
828
828
 
829
829
  const getCurrentValue = () => {
830
830
  const value = subscription.getCurrentValue();
@@ -370,6 +370,53 @@ describe("useSuspenseCoState", () => {
370
370
  });
371
371
  });
372
372
 
373
+ it("should throw error when CoValue becomes unauthorized", async () => {
374
+ const TestMap = co.map({
375
+ value: z.string(),
376
+ });
377
+
378
+ const group = Group.create();
379
+ group.addMember("everyone", "reader");
380
+
381
+ // Create CoValue owned by another account without sharing
382
+ const map = TestMap.create(
383
+ {
384
+ value: "123",
385
+ },
386
+ group,
387
+ );
388
+
389
+ await createJazzTestAccount({
390
+ isCurrentActiveAccount: true,
391
+ });
392
+
393
+ const TestComponent = () => {
394
+ const value = useSuspenseCoState(TestMap, map.$jazz.id);
395
+ return <div>{value.value}</div>;
396
+ };
397
+
398
+ const { container } = await act(async () => {
399
+ return render(
400
+ <ErrorBoundary fallback={<div>Error!</div>}>
401
+ <Suspense fallback={<div>Loading...</div>}>
402
+ <TestComponent />
403
+ </Suspense>
404
+ </ErrorBoundary>,
405
+ );
406
+ });
407
+ await waitFor(() => {
408
+ expect(container.textContent).toContain("123");
409
+ expect(container.textContent).not.toContain("Loading...");
410
+ });
411
+
412
+ group.removeMember("everyone");
413
+
414
+ // Wait for error to be thrown (unauthorized access)
415
+ await waitFor(() => {
416
+ expect(container.textContent).toContain("Error!");
417
+ });
418
+ });
419
+
373
420
  it("should update value when CoValue changes", async () => {
374
421
  const TestMap = co.map({
375
422
  value: z.string(),