Compare commits
35 Commits
refactor/i
...
jazz-581-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f56b9be0 | ||
|
|
4cae6bad34 | ||
|
|
17f2ef57de | ||
|
|
3a4d111a37 | ||
|
|
1e18c7f5fc | ||
|
|
8c7a6b27ed | ||
|
|
91f96e1188 | ||
|
|
28dac10723 | ||
|
|
9cb11e38dd | ||
|
|
f3e4bacb33 | ||
|
|
626d43f07b | ||
|
|
1f5d073035 | ||
|
|
a3b607e799 | ||
|
|
8fb93502af | ||
|
|
36774122e0 | ||
|
|
a6923128c1 | ||
|
|
706ca62feb | ||
|
|
01523dcca3 | ||
|
|
77f039b561 | ||
|
|
d661ba77be | ||
|
|
f8fbc59b6f | ||
|
|
cce0d22007 | ||
|
|
e3ff76e9cb | ||
|
|
4cbf71bff7 | ||
|
|
ceb060243a | ||
|
|
a70bebb96a | ||
|
|
b3b2507c35 | ||
|
|
6a8fa16b49 | ||
|
|
1f08807701 | ||
|
|
ba4a7f6170 | ||
|
|
a2854e3602 | ||
|
|
4ea87dc494 | ||
|
|
d8c87c5314 | ||
|
|
46f624a12e | ||
|
|
86ce770f38 |
6
.changeset/breezy-boxes-unite.md
Normal file
6
.changeset/breezy-boxes-unite.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"jazz-tools": minor
|
||||
"cojson": minor
|
||||
---
|
||||
|
||||
Check CoValue access permissions when loading
|
||||
5
.changeset/fast-beans-decide.md
Normal file
5
.changeset/fast-beans-decide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"jazz-tools": minor
|
||||
---
|
||||
|
||||
Implement new API for deep loading
|
||||
5
.changeset/funny-birds-flash.md
Normal file
5
.changeset/funny-birds-flash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cojson": minor
|
||||
---
|
||||
|
||||
Return the EVERYONE role if the account is not direct a member of the group
|
||||
@@ -28,7 +28,7 @@ export default function Conversation() {
|
||||
const { me } = useAccount();
|
||||
const [chat, setChat] = useState<Chat>();
|
||||
const [message, setMessage] = useState("");
|
||||
const loadedChat = useCoState(Chat, chat?.id, [{}]);
|
||||
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
|
||||
const navigation = useNavigation();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Conversation() {
|
||||
|
||||
const loadChat = async (chatId: ID<Chat>) => {
|
||||
try {
|
||||
const chat = await Chat.load(chatId, me, []);
|
||||
const chat = await Chat.load(chatId, me);
|
||||
setChat(chat);
|
||||
} catch (error) {
|
||||
console.log("Error loading chat", error);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Chat, Message } from "./schema";
|
||||
export default function ChatScreen({ navigation }: { navigation: any }) {
|
||||
const { me, logOut } = useAccount();
|
||||
const [chatId, setChatId] = useState<ID<Chat>>();
|
||||
const loadedChat = useCoState(Chat, chatId, [{}]);
|
||||
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
|
||||
const [message, setMessage] = useState("");
|
||||
const profile = useCoState(Profile, me._refs.profile?.id, {});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chat = useCoState(Chat, props.chatId, [{}]);
|
||||
const chat = useCoState(Chat, props.chatId, { resolve: { $each: true } });
|
||||
const showNLastMessages = ref(30);
|
||||
|
||||
const displayedMessages = computed(() => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "./ui.tsx";
|
||||
|
||||
export function ChatScreen(props: { chatID: ID<Chat> }) {
|
||||
const chat = useCoState(Chat, props.chatID, [{}]);
|
||||
const chat = useCoState(Chat, props.chatID, { resolve: { $each: true } });
|
||||
const [showNLastMessages, setShowNLastMessages] = useState(30);
|
||||
|
||||
if (!chat)
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
} from "./schema.ts";
|
||||
|
||||
export function CreateOrder() {
|
||||
const { me } = useAccount({ root: { draft: {}, orders: [] } });
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { draft: true, orders: true } },
|
||||
});
|
||||
const router = useIframeHashRouter();
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
@@ -60,7 +62,7 @@ function CreateOrderForm({
|
||||
onSave: (draft: DraftBubbleTeaOrder) => void;
|
||||
}) {
|
||||
const draft = useCoState(DraftBubbleTeaOrder, id, {
|
||||
addOns: [],
|
||||
resolve: { addOns: true },
|
||||
});
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAccount } from "jazz-react";
|
||||
|
||||
export function DraftIndicator() {
|
||||
const { me } = useAccount({
|
||||
root: { draft: {} },
|
||||
resolve: { root: { draft: true } },
|
||||
});
|
||||
|
||||
if (me?.root.draft?.hasChanges) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
import { BubbleTeaOrder } from "./schema.ts";
|
||||
|
||||
export function EditOrder(props: { id: ID<BubbleTeaOrder> }) {
|
||||
const order = useCoState(BubbleTeaOrder, props.id, []);
|
||||
const order = useCoState(BubbleTeaOrder, props.id);
|
||||
|
||||
if (!order) return;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
|
||||
export function Orders() {
|
||||
const { me } = useAccount({
|
||||
root: { orders: [] },
|
||||
resolve: { root: { orders: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,10 +24,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
* access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
*/
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {},
|
||||
playlists: [],
|
||||
},
|
||||
resolve: { root: { rootPlaylist: true, playlists: true } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -51,8 +48,9 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
|
||||
const params = useParams<{ playlistId: ID<Playlist> }>();
|
||||
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
|
||||
|
||||
const playlist = useCoState(Playlist, playlistId, {
|
||||
tracks: [],
|
||||
resolve: { tracks: true },
|
||||
});
|
||||
|
||||
const isRootPlaylist = !params.playlistId;
|
||||
|
||||
@@ -27,9 +27,11 @@ export async function uploadMusicTracks(
|
||||
isExampleTrack: boolean = false,
|
||||
) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,8 +67,10 @@ export async function uploadMusicTracks(
|
||||
|
||||
export async function createNewPlaylist() {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,9 +153,11 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
||||
|
||||
export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
rootPlaylist: {},
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: true,
|
||||
rootPlaylist: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,7 +166,9 @@ export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
|
||||
export async function updateActiveTrack(track: MusicTrack) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: {},
|
||||
},
|
||||
});
|
||||
|
||||
root.activeTrack = track;
|
||||
@@ -170,17 +178,23 @@ export async function onAnonymousAccountDiscarded(
|
||||
anonymousAccount: MusicaAccount,
|
||||
) {
|
||||
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getNextTrack, getPrevTrack } from "./lib/getters";
|
||||
|
||||
export function useMediaPlayer() {
|
||||
const { me } = useAccount({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
const playState = usePlayState();
|
||||
|
||||
@@ -16,8 +16,10 @@ export function InvitePage() {
|
||||
const playlist = await Playlist.load(playlistId, {});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,13 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,9 +30,7 @@ export function MusicTrackRow({
|
||||
const track = useCoState(MusicTrack, trackId);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
const playlists = me?.root.playlists ?? [];
|
||||
|
||||
@@ -12,9 +12,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
const activePlaylist = useAccount({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
},
|
||||
resolve: { root: { activePlaylist: true } },
|
||||
}).me?.root.activePlaylist;
|
||||
|
||||
useMediaEndListener(mediaPlayer.playNextTrack);
|
||||
@@ -25,7 +23,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
|
||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
||||
waveform: {},
|
||||
resolve: { waveform: true },
|
||||
});
|
||||
|
||||
if (!activeTrack) return null;
|
||||
|
||||
@@ -6,9 +6,7 @@ export function SidePanel() {
|
||||
const { playlistId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
@@ -8,7 +8,6 @@ export function Waveform(props: { track: MusicTrack; height: number }) {
|
||||
const waveformData = useCoState(
|
||||
MusicTrackWaveform,
|
||||
track._refs.waveform.id,
|
||||
{},
|
||||
)?.data;
|
||||
const duration = track.duration;
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { MusicaAccount } from "../1_schema";
|
||||
|
||||
export async function getNextTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -21,9 +23,11 @@ export async function getNextTrack() {
|
||||
|
||||
export async function getPrevTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useUploadExampleData() {
|
||||
|
||||
async function uploadOnboardingData() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
if (me.root.exampleDataLoaded) return;
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Organization } from "./schema.ts";
|
||||
|
||||
export function AcceptInvitePage() {
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({ root: { organizations: [] } });
|
||||
const { me } = useAccount({ resolve: { root: { organizations: true } } });
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
Organization.load(organizationId, me, []).then((organization) => {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
// avoid duplicates
|
||||
const ids = me.root.organizations.map(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Heading } from "./components/Heading.tsx";
|
||||
|
||||
export function HomePage() {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: true } },
|
||||
});
|
||||
|
||||
if (!me?.root.organizations) return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { me, logOut } = useAccount({
|
||||
root: { draftOrganization: {} },
|
||||
resolve: { root: { draftOrganization: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ export function OrganizationPage() {
|
||||
.organizationId;
|
||||
|
||||
const organization = useCoState(Organization, paramOrganizationId, {
|
||||
projects: [],
|
||||
resolve: { projects: true },
|
||||
});
|
||||
|
||||
if (!organization) return <p>Loading organization...</p>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { OrganizationForm } from "./OrganizationForm.tsx";
|
||||
|
||||
export function CreateOrganization() {
|
||||
const { me } = useAccount({
|
||||
root: { draftOrganization: {}, organizations: [] },
|
||||
resolve: { root: { draftOrganization: true, organizations: true } },
|
||||
});
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationSelector({ className }: { className?: string }) {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: { $each: true } } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -43,9 +43,11 @@ const VaultPage: React.FC = () => {
|
||||
(item): item is Exclude<typeof item, null> => !!item,
|
||||
) || [],
|
||||
);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, [
|
||||
{ items: [{}] },
|
||||
]);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, {
|
||||
resolve: {
|
||||
$each: { items: { $each: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedFolder, setSelectedFolder] = useState<Folder | undefined>();
|
||||
const [isNewItemModalOpen, setIsNewItemModalOpen] = useState(false);
|
||||
|
||||
@@ -60,11 +60,9 @@ export async function addSharedFolder(
|
||||
me: PasswordManagerAccount,
|
||||
) {
|
||||
const [sharedFolder, account] = await Promise.all([
|
||||
Folder.load(sharedFolderId, me, {}),
|
||||
PasswordManagerAccount.load(me.id, me, {
|
||||
root: {
|
||||
folders: [],
|
||||
},
|
||||
Folder.load(sharedFolderId),
|
||||
PasswordManagerAccount.load(me.id, {
|
||||
resolve: { root: { folders: true } },
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const reactionEmojiMap: {
|
||||
};
|
||||
|
||||
export function ReactionsScreen(props: { id: ID<Reactions> }) {
|
||||
const reactions = useCoState(Reactions, props.id, []);
|
||||
const reactions = useCoState(Reactions, props.id);
|
||||
|
||||
if (!reactions) return;
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<div class="section-header">
|
||||
<h2>Folders</h2>
|
||||
<div class="new-folder">
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createFolder">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="folder-list">
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="section-header">
|
||||
<h2>{{ selectedFolder?.name }}</h2>
|
||||
<div class="new-todo">
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createTodo">Add</button>
|
||||
@@ -72,7 +72,9 @@ import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
|
||||
const { me } = useAccount();
|
||||
|
||||
const computedFoldersId = computed(() => me.value?.root?.folders?.id);
|
||||
const folders = useCoState(FolderList, computedFoldersId, [{ items: [{}] }]);
|
||||
const folders = useCoState(FolderList, computedFoldersId, {
|
||||
resolve: { $each: { items: true } },
|
||||
});
|
||||
|
||||
const selectedFolder = ref<Folder>();
|
||||
const newFolderName = ref("");
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function App() {
|
||||
|
||||
function HomeScreen() {
|
||||
const { me } = useAccount({
|
||||
root: { projects: [{}] },
|
||||
resolve: { root: { projects: { $each: true } } },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import { ID } from "jazz-tools";
|
||||
import { IssueComponent } from "./Issue.tsx";
|
||||
import { Issue, Project } from "./schema.ts";
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, {
|
||||
resolve: { issues: { $each: true } },
|
||||
});
|
||||
|
||||
if (!project) return;
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = issue.subscribe([], (updatedIssue) => console.log(updatedIssue));
|
||||
const unsub = issue.subscribe({ resolve: true }, (updatedIssue) => console.log(updatedIssue));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -339,7 +339,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = Issue.subscribe(issueID, me, [], (updatedIssue) => {
|
||||
const unsub = Issue.subscribe(issueID, me, { resolve: true }, (updatedIssue) => {
|
||||
console.log(updatedIssue);
|
||||
});
|
||||
```
|
||||
@@ -358,7 +358,7 @@ This works because CoValues
|
||||
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
|
||||
const [value, setValue] = useState<V>();
|
||||
|
||||
useEffect(() => Schema.subscribe(id, [], setValue), [id]);
|
||||
useEffect(() => Schema.subscribe(id, { resolve: true }, setValue), [id]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -454,7 +454,7 @@ function App() { // old
|
||||
const issue = useCoState(Issue, issueID); // old
|
||||
// old
|
||||
const createIssue = () => { // old
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer");
|
||||
// old
|
||||
const newIssue = Issue.create( // old
|
||||
@@ -605,6 +605,8 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : project === null ? (
|
||||
<div>Project not found or access denied</div>
|
||||
) : (
|
||||
<div>Loading project...</div>
|
||||
);
|
||||
@@ -635,7 +637,7 @@ import { IssueComponent } from "./Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
// old
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
|
||||
|
||||
const createAndAddIssue = () => {// old
|
||||
project?.issues.push(Issue.create({
|
||||
@@ -663,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
|
||||
The loading-depth spec `{ resolve: { issues: { $each: true } } }` means "in `Project`, load `issues` and load each item in `issues` deeply". (Since an `Issue` doesn't have any further references, "deeply" actually means all its properties will be available).
|
||||
|
||||
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
|
||||
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
|
||||
@@ -747,7 +749,7 @@ import { createInviteLink } from "jazz-react";
|
||||
// old
|
||||
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] }); // old
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } }); // old
|
||||
|
||||
const { me } = useAccount();
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Jazz 0.11.0 - Deeply resolved data" };
|
||||
|
||||
# Jazz 0.11.0 - Deeply resolved data
|
||||
|
||||
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
|
||||
15 March 2025
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
Jazz 0.11.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable.
|
||||
|
||||
<h3>What's new?</h3>
|
||||
- New resolve API for a more type-safe deep loading
|
||||
- Improved permission checks on deep loading
|
||||
- Easier type safety with the Resolved type
|
||||
</div>
|
||||
|
||||
<h3>New Resolve API</h3>
|
||||
<div>
|
||||
We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs.
|
||||
|
||||
To get started with the new resolve API, replace empty array/object parameters with structured resolve configs:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Before
|
||||
const playlist = useCoState(Playlist, id, [{}]);
|
||||
const { me } = useAccount({ root: { playlists: [] } });
|
||||
|
||||
// After
|
||||
const playlist = useCoState(Playlist, id, {
|
||||
resolve: { $each: true }
|
||||
});
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { playlists: true } }
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The new API works across all loading methods:
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
// Before
|
||||
Playlist.load(id, account, {
|
||||
tracks: [],
|
||||
});
|
||||
|
||||
// After
|
||||
Playlist.load(id, {
|
||||
loadAs: account,
|
||||
resolve: { tracks: true }
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</div>
|
||||
|
||||
<h3>Improved permission checks on deep loading</h3>
|
||||
<div>
|
||||
Now `useCoState` will return `null` when the current user lacks permissions to load the requested data.
|
||||
|
||||
Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing.
|
||||
|
||||
Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing.
|
||||
|
||||
We also have implemented a more granular permission checking, where if an optional coValue cannot be accessed, useCoState will return the data stripped of that coValue.
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
class ListOfTracks extends CoList.Of(co.optional.ref(Track)) {}
|
||||
// Before (ambiguous states)
|
||||
const value = useCoState(ListOfTracks, id, [{}]);
|
||||
if (value === undefined) return <div>Loading or access denied</div>;
|
||||
if (value === null) return <div>Not found</div>;
|
||||
|
||||
// After
|
||||
const value = useCoState(ListOfTracks, id, { resolve: { $each: true } });
|
||||
if (value === undefined) return <div>Loading...</div>;
|
||||
if (value === null) return <div>Not found or access denied</div>;
|
||||
|
||||
// If the current user lacks permissions to load a Track, the ListOfTracks will be returned without that Track.
|
||||
return tracks.map(track => track && <MusicTrack track={track} />);
|
||||
```
|
||||
</CodeGroup>
|
||||
</div>
|
||||
|
||||
<h3>Type Safety Improvements</h3>
|
||||
<div>
|
||||
The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
// Before
|
||||
type PlaylistResolved = Resolved<Playlist, {
|
||||
tracks: { $each: true }
|
||||
}>;
|
||||
|
||||
// After
|
||||
function TrackList({ playlist }: { playlist: PlaylistResolved }) {
|
||||
// Safe access to resolved tracks
|
||||
return playlist.tracks.map(track => /* ... */);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</div>
|
||||
@@ -66,6 +66,12 @@ export const docNavigationItems = [
|
||||
{
|
||||
name: "Upgrade guides",
|
||||
items: [
|
||||
{
|
||||
// upgrade guides
|
||||
name: "0.12.0 - Deeply Resolved Data",
|
||||
href: "/docs/upgrade/0-12-0",
|
||||
done: 100,
|
||||
},
|
||||
{
|
||||
// upgrade guides
|
||||
name: "0.11.0 - Roles and permissions",
|
||||
|
||||
@@ -238,7 +238,7 @@ export class RawCoMapView<
|
||||
get<K extends keyof Shape & string>(key: K): Shape[K] | undefined {
|
||||
const entry = this.getRaw(key);
|
||||
|
||||
if (entry === undefined) {
|
||||
if (entry?.change === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ export class RawGroup<
|
||||
if (!roleInfo && accountID !== "everyone") {
|
||||
const everyoneRole = this.get("everyone");
|
||||
|
||||
if (everyoneRole) return { role: everyoneRole, via: undefined };
|
||||
if (everyoneRole && everyoneRole !== "revoked")
|
||||
return { role: everyoneRole, via: undefined };
|
||||
}
|
||||
|
||||
return roleInfo;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RawCoMap } from "../coValues/coMap.js";
|
||||
import { RawCoStream } from "../coValues/coStream.js";
|
||||
import { RawBinaryCoStream } from "../coValues/coStream.js";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||
import { RawAccountID } from "../exports.js";
|
||||
import { LocalNode } from "../localNode.js";
|
||||
import {
|
||||
createThreeConnectedNodes,
|
||||
@@ -915,117 +916,215 @@ describe("extend with role mapping", () => {
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
});
|
||||
test("roleOf should prioritize explicit account role over everyone role in same group", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
describe("roleOf", () => {
|
||||
test("returns direct role assignments", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
// Add both everyone and specific account
|
||||
group.addMember("everyone", "reader");
|
||||
group.addMember(account2, "writer");
|
||||
group.addMember(account, "writer");
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
// Should return the explicit role, not everyone's role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
test("returns undefined for non-members", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
// Change everyone's role
|
||||
group.addMember("everyone", "writer");
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
// Should still return the explicit role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should prioritize inherited everyone role over explicit account role", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent and account to child
|
||||
parentGroup.addMember("everyone", "writer");
|
||||
childGroup.addMember(account2, "reader");
|
||||
|
||||
// Should return the explicit role from child, not inherited everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should use everyone role when no explicit role exists", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
// Add only everyone role
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
// Should return everyone's role when no explicit role exists
|
||||
expect(group.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should inherit everyone role from parent when no explicit roles exist", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent only
|
||||
parentGroup.addMember("everyone", "reader");
|
||||
|
||||
// Should inherit everyone's role from parent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should handle everyone role inheritance through multiple levels", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const grandParentGroup = node1.node.createGroup();
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(node2.node, childGroup.id);
|
||||
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance chain
|
||||
parentGroup.extend(grandParentGroup);
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to grandparent
|
||||
grandParentGroup.addMember("everyone", "writer");
|
||||
|
||||
// Should inherit everyone's role from grandparent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in parent
|
||||
parentGroup.addMember(account2, "reader");
|
||||
|
||||
// Should use parent's explicit role instead of grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in child
|
||||
childGroup.addMember(account2, "admin");
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should use child's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
// Remove child's explicit role
|
||||
await childGroupOnNode2.removeMember(account2);
|
||||
await childGroupOnNode2.core.waitForSync();
|
||||
|
||||
// Should fall back to parent's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Remove parent's explicit role
|
||||
await parentGroup.removeMember(account2);
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should fall back to grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
test("revoked roles return undefined", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
group.addMember(account, "writer");
|
||||
group.removeMemberInternal(account);
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("everyone role applies to all accounts", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("account role overrides everyone role", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
group.addMemberInternal("everyone", "writer");
|
||||
group.addMember(account, "reader");
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("Revoking access on everyone role should not affect existing members", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
group.addMemberInternal("everyone", "reader");
|
||||
group.addMember(account, "writer");
|
||||
group.removeMemberInternal("everyone");
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual("writer");
|
||||
expect(group.roleOf("123" as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Everyone role is inherited following the most permissive algorithm", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
const parentGroup = node.createGroup();
|
||||
parentGroup.addMemberInternal("everyone", "writer");
|
||||
|
||||
group.extend(parentGroup);
|
||||
group.addMember(account, "reader");
|
||||
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual("writer");
|
||||
});
|
||||
test("roleOf should prioritize explicit account role over everyone role in same group", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Add both everyone and specific account
|
||||
group.addMember("everyone", "reader");
|
||||
group.addMember(account2, "writer");
|
||||
|
||||
// Should return the explicit role, not everyone's role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Change everyone's role
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
// Should still return the explicit role
|
||||
expect(group.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should prioritize inherited everyone role over explicit account role", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent and account to child
|
||||
parentGroup.addMember("everyone", "writer");
|
||||
childGroup.addMember(account2, "reader");
|
||||
|
||||
// Should return the explicit role from child, not inherited everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
|
||||
test("roleOf should use everyone role when no explicit role exists", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const group = node1.node.createGroup();
|
||||
|
||||
// Add only everyone role
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
// Should return everyone's role when no explicit role exists
|
||||
expect(group.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should inherit everyone role from parent when no explicit roles exist", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
// Set up inheritance
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to parent only
|
||||
parentGroup.addMember("everyone", "reader");
|
||||
|
||||
// Should inherit everyone's role from parent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("roleOf should handle everyone role inheritance through multiple levels", async () => {
|
||||
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
|
||||
|
||||
const grandParentGroup = node1.node.createGroup();
|
||||
const parentGroup = node1.node.createGroup();
|
||||
const childGroup = node1.node.createGroup();
|
||||
|
||||
const childGroupOnNode2 = await loadCoValueOrFail(
|
||||
node2.node,
|
||||
childGroup.id,
|
||||
);
|
||||
|
||||
const account2 = await loadCoValueOrFail(node1.node, node2.accountID);
|
||||
|
||||
// Set up inheritance chain
|
||||
parentGroup.extend(grandParentGroup);
|
||||
childGroup.extend(parentGroup);
|
||||
|
||||
// Add everyone to grandparent
|
||||
grandParentGroup.addMember("everyone", "writer");
|
||||
|
||||
// Should inherit everyone's role from grandparent
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in parent
|
||||
parentGroup.addMember(account2, "reader");
|
||||
|
||||
// Should use parent's explicit role instead of grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Add explicit role in child
|
||||
childGroup.addMember(account2, "admin");
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should use child's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("admin");
|
||||
|
||||
// Remove child's explicit role
|
||||
await childGroupOnNode2.removeMember(account2);
|
||||
await childGroupOnNode2.core.waitForSync();
|
||||
|
||||
// Should fall back to parent's explicit role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
|
||||
// Remove parent's explicit role
|
||||
await parentGroup.removeMember(account2);
|
||||
await childGroup.core.waitForSync();
|
||||
|
||||
// Should fall back to grandparent's everyone role
|
||||
expect(childGroup.roleOf(node2.accountID)).toEqual("writer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +107,9 @@ export class JazzClerkAuth {
|
||||
});
|
||||
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
const username = getClerkUsername(clerkClient);
|
||||
|
||||
@@ -81,7 +81,9 @@ describe("JazzClerkAuth", () => {
|
||||
});
|
||||
|
||||
const me = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
expect(me.profile.name).toBe("Guido");
|
||||
});
|
||||
|
||||
@@ -83,7 +83,9 @@ export class BrowserPasskeyAuth {
|
||||
});
|
||||
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (username.trim().length !== 0) {
|
||||
|
||||
@@ -198,7 +198,9 @@ describe("BrowserPasskeyAuth", () => {
|
||||
await auth.signUp("");
|
||||
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 'Test Account' is the name provided during account creation (see: `jazz-tools/src/testing.ts`)
|
||||
@@ -228,7 +230,9 @@ describe("BrowserPasskeyAuth", () => {
|
||||
await auth.signUp("testuser");
|
||||
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(currentAccount.profile.name).toEqual("testuser");
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("startWorker integration", () => {
|
||||
|
||||
await map.waitForSync();
|
||||
|
||||
const mapOnWorker2 = await TestMap.load(map.id, worker2.worker, {});
|
||||
const mapOnWorker2 = await TestMap.load(map.id, { loadAs: worker2.worker });
|
||||
|
||||
expect(mapOnWorker2?.value).toBe("test");
|
||||
|
||||
@@ -113,7 +113,9 @@ describe("startWorker integration", () => {
|
||||
|
||||
const worker1 = await setup(CustomAccount);
|
||||
|
||||
const { root } = await worker1.worker.ensureLoaded({ root: {} });
|
||||
const { root } = await worker1.worker.ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
expect(root.value).toBe("test");
|
||||
|
||||
@@ -126,7 +128,9 @@ describe("startWorker integration", () => {
|
||||
AccountSchema: CustomAccount,
|
||||
});
|
||||
|
||||
const { root: root2 } = await worker2.worker.ensureLoaded({ root: {} });
|
||||
const { root: root2 } = await worker2.worker.ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
expect(root2.value).toBe("test");
|
||||
|
||||
@@ -150,7 +154,7 @@ describe("startWorker integration", () => {
|
||||
|
||||
const worker2 = await setupWorker(worker1.syncServer);
|
||||
|
||||
const mapOnWorker2 = await TestMap.load(map.id, worker2.worker, {});
|
||||
const mapOnWorker2 = await TestMap.load(map.id, { loadAs: worker2.worker });
|
||||
|
||||
expect(mapOnWorker2?.value).toBe("test");
|
||||
|
||||
@@ -185,7 +189,7 @@ describe("startWorker integration", () => {
|
||||
|
||||
const resultId = await sender.sendMessage(map);
|
||||
|
||||
const result = await TestMap.load(resultId, worker2.worker, {});
|
||||
const result = await TestMap.load(resultId, { loadAs: worker2.worker });
|
||||
|
||||
expect(result?.value).toEqual("Hello! Responded from the inbox");
|
||||
|
||||
@@ -231,8 +235,10 @@ describe("startWorker integration", () => {
|
||||
await map2.waitForSync();
|
||||
|
||||
// Verify both old and new values are synced
|
||||
const mapOnWorker2 = await TestMap.load(map.id, worker2.worker, {});
|
||||
const map2OnWorker2 = await TestMap.load(map2.id, worker2.worker, {});
|
||||
const mapOnWorker2 = await TestMap.load(map.id, { loadAs: worker2.worker });
|
||||
const map2OnWorker2 = await TestMap.load(map2.id, {
|
||||
loadAs: worker2.worker,
|
||||
});
|
||||
|
||||
expect(mapOnWorker2?.value).toBe("initial value");
|
||||
expect(map2OnWorker2?.value).toBe("created while offline");
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
AnonymousJazzAgent,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
InboxSender,
|
||||
JazzContextManager,
|
||||
JazzContextType,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
createCoValueObservable,
|
||||
} from "jazz-tools";
|
||||
import {
|
||||
@@ -80,12 +81,11 @@ export function useIsAuthenticated() {
|
||||
);
|
||||
}
|
||||
|
||||
function useCoValueObservable<V extends CoValue, D>() {
|
||||
const [initialValue] = React.useState(() =>
|
||||
createCoValueObservable<V, D>({
|
||||
syncResolution: true,
|
||||
}),
|
||||
);
|
||||
function useCoValueObservable<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>() {
|
||||
const [initialValue] = React.useState(() => createCoValueObservable<V, R>());
|
||||
const ref = useRef(initialValue);
|
||||
|
||||
return {
|
||||
@@ -96,26 +96,25 @@ function useCoValueObservable<V extends CoValue, D>() {
|
||||
return ref.current;
|
||||
},
|
||||
reset() {
|
||||
ref.current = createCoValueObservable<V, D>({
|
||||
syncResolution: true,
|
||||
});
|
||||
ref.current = createCoValueObservable<V, R>();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useCoState<V extends CoValue, D>(
|
||||
export function useCoState<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V> = true,
|
||||
>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Schema: CoValueClass<V>,
|
||||
id: ID<CoValue> | undefined,
|
||||
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
|
||||
): DeeplyLoaded<V, D> | undefined | null {
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> },
|
||||
): Resolved<V, R> | undefined | null {
|
||||
const contextManager = useJazzContextManager();
|
||||
|
||||
const observable = useCoValueObservable<V, D>();
|
||||
const observable = useCoValueObservable<V, R>();
|
||||
|
||||
const value = React.useSyncExternalStore<
|
||||
DeeplyLoaded<V, D> | undefined | null
|
||||
>(
|
||||
const value = React.useSyncExternalStore<Resolved<V, R> | undefined | null>(
|
||||
React.useCallback(
|
||||
(callback) => {
|
||||
if (!id) return () => {};
|
||||
@@ -125,12 +124,20 @@ export function useCoState<V extends CoValue, D>(
|
||||
// up to date with the data when logging in and out.
|
||||
return subscribeToContextManager(contextManager, () => {
|
||||
const agent = getCurrentAccountFromContextManager(contextManager);
|
||||
|
||||
observable.reset();
|
||||
|
||||
return observable
|
||||
.getCurrentObservable()
|
||||
.subscribe(Schema, id, agent, depth, callback, callback);
|
||||
return observable.getCurrentObservable().subscribe(
|
||||
Schema,
|
||||
id,
|
||||
{
|
||||
loadAs: agent,
|
||||
resolve: options?.resolve,
|
||||
onUnauthorized: callback,
|
||||
onUnavailable: callback,
|
||||
syncResolution: true,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
});
|
||||
},
|
||||
[Schema, id, contextManager],
|
||||
@@ -146,12 +153,18 @@ function useAccount<A extends RegisteredAccount>(): {
|
||||
me: A;
|
||||
logOut: () => void;
|
||||
};
|
||||
function useAccount<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
depth: D,
|
||||
): { me: DeeplyLoaded<A, D> | undefined | null; logOut: () => void };
|
||||
function useAccount<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
depth?: D,
|
||||
): { me: A | DeeplyLoaded<A, D> | undefined | null; logOut: () => void } {
|
||||
function useAccount<
|
||||
A extends RegisteredAccount,
|
||||
R extends RefsToResolve<A>,
|
||||
>(options?: {
|
||||
resolve?: RefsToResolveStrict<A, R>;
|
||||
}): { me: Resolved<A, R> | undefined | null; logOut: () => void };
|
||||
function useAccount<
|
||||
A extends RegisteredAccount,
|
||||
R extends RefsToResolve<A>,
|
||||
>(options?: {
|
||||
resolve?: RefsToResolveStrict<A, R>;
|
||||
}): { me: A | Resolved<A, R> | undefined | null; logOut: () => void } {
|
||||
const context = useJazzContext<A>();
|
||||
const contextManager = useJazzContextManager<A>();
|
||||
|
||||
@@ -161,9 +174,9 @@ function useAccount<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
);
|
||||
}
|
||||
|
||||
const observable = useCoValueObservable<A, D>();
|
||||
const observable = useCoValueObservable<A, R>();
|
||||
|
||||
const me = React.useSyncExternalStore<DeeplyLoaded<A, D> | undefined | null>(
|
||||
const me = React.useSyncExternalStore<Resolved<A, R> | undefined | null>(
|
||||
React.useCallback(
|
||||
(callback) => {
|
||||
return subscribeToContextManager(contextManager, () => {
|
||||
@@ -179,16 +192,18 @@ function useAccount<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
|
||||
const Schema = agent.constructor as CoValueClass<A>;
|
||||
|
||||
return observable
|
||||
.getCurrentObservable()
|
||||
.subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
agent,
|
||||
depth ?? ([] as D),
|
||||
callback,
|
||||
callback,
|
||||
);
|
||||
return observable.getCurrentObservable().subscribe(
|
||||
Schema,
|
||||
(agent as A).id,
|
||||
{
|
||||
loadAs: agent,
|
||||
resolve: options?.resolve,
|
||||
onUnauthorized: callback,
|
||||
onUnavailable: callback,
|
||||
syncResolution: true,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
});
|
||||
},
|
||||
[contextManager],
|
||||
@@ -198,28 +213,32 @@ function useAccount<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
);
|
||||
|
||||
return {
|
||||
me: depth === undefined ? me || context.me : me,
|
||||
logOut: context.logOut,
|
||||
me: options?.resolve === undefined ? me || context.me : me,
|
||||
logOut: contextManager.logOut,
|
||||
};
|
||||
}
|
||||
|
||||
function useAccountOrGuest<A extends RegisteredAccount>(): {
|
||||
me: A | AnonymousJazzAgent;
|
||||
};
|
||||
function useAccountOrGuest<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
depth: D,
|
||||
): { me: DeeplyLoaded<A, D> | undefined | null | AnonymousJazzAgent };
|
||||
function useAccountOrGuest<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
depth?: D,
|
||||
): {
|
||||
me: A | DeeplyLoaded<A, D> | undefined | null | AnonymousJazzAgent;
|
||||
function useAccountOrGuest<
|
||||
A extends RegisteredAccount,
|
||||
R extends RefsToResolve<A>,
|
||||
>(options?: { resolve?: RefsToResolveStrict<A, R> }): {
|
||||
me: Resolved<A, R> | undefined | null | AnonymousJazzAgent;
|
||||
};
|
||||
function useAccountOrGuest<
|
||||
A extends RegisteredAccount,
|
||||
R extends RefsToResolve<A>,
|
||||
>(options?: { resolve?: RefsToResolveStrict<A, R> }): {
|
||||
me: A | Resolved<A, R> | undefined | null | AnonymousJazzAgent;
|
||||
} {
|
||||
const context = useJazzContext<A>();
|
||||
const contextManager = useJazzContextManager<A>();
|
||||
|
||||
const observable = useCoValueObservable<A, D>();
|
||||
const observable = useCoValueObservable<A, R>();
|
||||
|
||||
const me = React.useSyncExternalStore<DeeplyLoaded<A, D> | undefined | null>(
|
||||
const me = React.useSyncExternalStore<Resolved<A, R> | undefined | null>(
|
||||
React.useCallback(
|
||||
(callback) => {
|
||||
return subscribeToContextManager(contextManager, () => {
|
||||
@@ -233,16 +252,18 @@ function useAccountOrGuest<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
|
||||
const Schema = agent.constructor as CoValueClass<A>;
|
||||
|
||||
return observable
|
||||
.getCurrentObservable()
|
||||
.subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
agent,
|
||||
depth ?? ([] as D),
|
||||
callback,
|
||||
callback,
|
||||
);
|
||||
return observable.getCurrentObservable().subscribe(
|
||||
Schema,
|
||||
(agent as A).id,
|
||||
{
|
||||
loadAs: agent,
|
||||
resolve: options?.resolve,
|
||||
onUnauthorized: callback,
|
||||
onUnavailable: callback,
|
||||
syncResolution: true,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
});
|
||||
},
|
||||
[contextManager],
|
||||
@@ -253,7 +274,7 @@ function useAccountOrGuest<A extends RegisteredAccount, D extends DepthsIn<A>>(
|
||||
|
||||
if ("me" in context) {
|
||||
return {
|
||||
me: depth === undefined ? me || context.me : me,
|
||||
me: options?.resolve === undefined ? me || context.me : me,
|
||||
};
|
||||
} else {
|
||||
return { me: context.guest };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Account, CoMap, DepthsIn, co } from "jazz-tools";
|
||||
import { Account, CoMap, RefsToResolve, Resolved, co } from "jazz-tools";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { useAccount, useJazzContextManager } from "../hooks.js";
|
||||
import { useIsAuthenticated } from "../index.js";
|
||||
@@ -41,8 +41,10 @@ describe("useAccount", () => {
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAccount<AccountSchema, DepthsIn<AccountSchema>>({
|
||||
root: {},
|
||||
useAccount<AccountSchema, RefsToResolve<{ root: true }>>({
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Account, CoMap, DepthsIn, co } from "jazz-tools";
|
||||
import { Account, CoMap, RefsToResolve, co } from "jazz-tools";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useAccountOrGuest } from "../index.js";
|
||||
import { createJazzTestAccount, createJazzTestGuest } from "../testing.js";
|
||||
@@ -46,8 +46,10 @@ describe("useAccountOrGuest", () => {
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAccountOrGuest<AccountSchema, DepthsIn<AccountSchema>>({
|
||||
root: {},
|
||||
useAccountOrGuest<AccountSchema, RefsToResolve<{ root: true }>>({
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
@@ -64,7 +66,9 @@ describe("useAccountOrGuest", () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAccountOrGuest({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cojsonInternals } from "cojson";
|
||||
import { CoMap, CoValue, ID, co } from "jazz-tools";
|
||||
import { CoMap, CoValue, Group, ID, co } from "jazz-tools";
|
||||
import { beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { useCoState } from "../index.js";
|
||||
import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
|
||||
@@ -91,7 +91,9 @@ describe("useCoState", () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCoState(TestMap, map.id, {
|
||||
nested: {},
|
||||
resolve: {
|
||||
nested: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
@@ -145,7 +147,7 @@ describe("useCoState", () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCoState(TestMap, (map.id + "123") as any, {}),
|
||||
() => useCoState(TestMap, (map.id + "123") as any),
|
||||
{
|
||||
account,
|
||||
},
|
||||
@@ -158,6 +160,168 @@ describe("useCoState", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null if the coValue is not accessible", async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = co.string;
|
||||
}
|
||||
|
||||
const someoneElse = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: "123",
|
||||
},
|
||||
someoneElse,
|
||||
);
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCoState(TestMap, map.id), {
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not return null if the coValue is shared with everyone", async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = co.string;
|
||||
}
|
||||
|
||||
const someoneElse = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const group = Group.create(someoneElse);
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: "123",
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCoState(TestMap, map.id), {
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current?.value).toBe("123");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return a value when the coValue becomes accessible", async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = co.string;
|
||||
}
|
||||
|
||||
const someoneElse = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const group = Group.create(someoneElse);
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: "123",
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCoState(TestMap, map.id), {
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeNull();
|
||||
expect(result.current?.value).toBe("123");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update when an inner coValue is updated", async () => {
|
||||
class TestMap extends CoMap {
|
||||
value = co.string;
|
||||
nested = co.optional.ref(TestMap);
|
||||
}
|
||||
|
||||
const someoneElse = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const everyone = Group.create(someoneElse);
|
||||
everyone.addMember("everyone", "reader");
|
||||
const group = Group.create(someoneElse);
|
||||
|
||||
const map = TestMap.create(
|
||||
{
|
||||
value: "123",
|
||||
nested: TestMap.create(
|
||||
{
|
||||
value: "456",
|
||||
},
|
||||
group,
|
||||
),
|
||||
},
|
||||
everyone,
|
||||
);
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCoState(TestMap, map.id, {
|
||||
resolve: {
|
||||
nested: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeUndefined();
|
||||
});
|
||||
|
||||
expect(result.current?.nested).toBeUndefined();
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current?.nested?.value).toBe("456");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the same type as Schema", () => {
|
||||
class TestMap extends CoMap {
|
||||
value = co.string;
|
||||
@@ -168,7 +332,7 @@ describe("useCoState", () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCoState(TestMap, map.id as ID<CoValue>, []),
|
||||
useCoState(TestMap, map.id as ID<CoValue>),
|
||||
);
|
||||
expectTypeOf(result).toEqualTypeOf<{
|
||||
current: TestMap | null | undefined;
|
||||
|
||||
@@ -45,7 +45,9 @@ describe("useInboxSender", () => {
|
||||
|
||||
expect(incoming.value).toEqual("hello");
|
||||
const response = await promise;
|
||||
const responseMap = await TestMap.load(response, account, {});
|
||||
const responseMap = await TestMap.load(response, {
|
||||
loadAs: account,
|
||||
});
|
||||
|
||||
expect(responseMap!.value).toEqual("got it");
|
||||
});
|
||||
|
||||
@@ -46,7 +46,9 @@ describe("useAcceptInvite", () => {
|
||||
expect(acceptedId).toBeDefined();
|
||||
});
|
||||
|
||||
const accepted = await TestMap.load(acceptedId!, account, {});
|
||||
const accepted = await TestMap.load(acceptedId!, {
|
||||
loadAs: account,
|
||||
});
|
||||
|
||||
expect(accepted?.value).toEqual("hello");
|
||||
});
|
||||
|
||||
@@ -4,16 +4,17 @@ import type {
|
||||
AuthSecretStorage,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
JazzAuthContext,
|
||||
JazzContextType,
|
||||
JazzGuestContext
|
||||
JazzGuestContext,
|
||||
RefsToResolve,
|
||||
Resolved
|
||||
} from 'jazz-tools';
|
||||
import { Account, subscribeToCoValue } from 'jazz-tools';
|
||||
import { getContext, untrack } from 'svelte';
|
||||
import Provider from './Provider.svelte';
|
||||
import type { RefsToResolveStrict } from 'jazz-tools';
|
||||
|
||||
export { Provider as JazzProvider };
|
||||
|
||||
@@ -71,21 +72,12 @@ declare module "jazz-tools" {
|
||||
}
|
||||
}
|
||||
|
||||
export function useAccount(): { me: RegisteredAccount; logOut: () => void };
|
||||
export function useAccount<D extends DepthsIn<RegisteredAccount>>(
|
||||
depth: D
|
||||
): { me: DeeplyLoaded<RegisteredAccount, D> | undefined | null; logOut: () => void };
|
||||
/**
|
||||
* Use the current account with a optional depth.
|
||||
* @param depth - The depth.
|
||||
* @returns The current account.
|
||||
*/
|
||||
export function useAccount<D extends DepthsIn<RegisteredAccount>>(
|
||||
depth?: D
|
||||
): {
|
||||
me: RegisteredAccount | DeeplyLoaded<RegisteredAccount, D> | undefined | null;
|
||||
logOut: () => void;
|
||||
} {
|
||||
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: Resolved<RegisteredAccount, R> | undefined | null; logOut: () => void };
|
||||
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined | null; logOut: () => void } {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
if (!ctx?.current) {
|
||||
throw new Error('useAccount must be used within a JazzProvider');
|
||||
@@ -97,7 +89,7 @@ export function useAccount<D extends DepthsIn<RegisteredAccount>>(
|
||||
}
|
||||
|
||||
// If no depth is specified, return the context's me directly
|
||||
if (depth === undefined) {
|
||||
if (options?.resolve === undefined) {
|
||||
return {
|
||||
get me() {
|
||||
return (ctx.current as JazzAuthContext<RegisteredAccount>).me;
|
||||
@@ -109,10 +101,10 @@ export function useAccount<D extends DepthsIn<RegisteredAccount>>(
|
||||
}
|
||||
|
||||
// If depth is specified, use useCoState to get the deeply loaded version
|
||||
const me = useCoState<RegisteredAccount, D>(
|
||||
const me = useCoState<RegisteredAccount, R>(
|
||||
ctx.current.me.constructor as CoValueClass<RegisteredAccount>,
|
||||
(ctx.current as JazzAuthContext<RegisteredAccount>).me.id,
|
||||
depth
|
||||
options
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -126,24 +118,12 @@ export function useAccount<D extends DepthsIn<RegisteredAccount>>(
|
||||
}
|
||||
|
||||
export function useAccountOrGuest(): { me: RegisteredAccount | AnonymousJazzAgent };
|
||||
export function useAccountOrGuest<D extends DepthsIn<RegisteredAccount>>(
|
||||
depth: D
|
||||
): { me: DeeplyLoaded<RegisteredAccount, D> | undefined | null | AnonymousJazzAgent };
|
||||
/**
|
||||
* Use the current account or guest with a optional depth.
|
||||
* @param depth - The depth.
|
||||
* @returns The current account or guest.
|
||||
*/
|
||||
export function useAccountOrGuest<D extends DepthsIn<RegisteredAccount>>(
|
||||
depth?: D
|
||||
): {
|
||||
me:
|
||||
| RegisteredAccount
|
||||
| DeeplyLoaded<RegisteredAccount, D>
|
||||
| undefined
|
||||
| null
|
||||
| AnonymousJazzAgent;
|
||||
} {
|
||||
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: Resolved<RegisteredAccount, R> | undefined | null| AnonymousJazzAgent };
|
||||
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
|
||||
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
|
||||
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined | null| AnonymousJazzAgent } {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
|
||||
if (!ctx?.current) {
|
||||
@@ -152,17 +132,17 @@ export function useAccountOrGuest<D extends DepthsIn<RegisteredAccount>>(
|
||||
|
||||
const contextMe = 'me' in ctx.current ? ctx.current.me : undefined;
|
||||
|
||||
const me = useCoState<RegisteredAccount, D>(
|
||||
const me = useCoState<RegisteredAccount, R>(
|
||||
contextMe?.constructor as CoValueClass<RegisteredAccount>,
|
||||
contextMe?.id,
|
||||
depth
|
||||
options
|
||||
);
|
||||
|
||||
// If the context has a me, return the account.
|
||||
if ('me' in ctx.current) {
|
||||
return {
|
||||
get me() {
|
||||
return depth === undefined
|
||||
return options?.resolve === undefined
|
||||
? me.current || (ctx.current as JazzAuthContext<RegisteredAccount>)?.me
|
||||
: me.current;
|
||||
}
|
||||
@@ -178,24 +158,17 @@ export function useAccountOrGuest<D extends DepthsIn<RegisteredAccount>>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a CoValue with a optional depth.
|
||||
* @param Schema - The CoValue schema.
|
||||
* @param id - The CoValue id.
|
||||
* @param depth - The depth.
|
||||
* @returns The CoValue.
|
||||
*/
|
||||
export function useCoState<V extends CoValue, D extends DepthsIn<V> = []>(
|
||||
export function useCoState<V extends CoValue, R extends RefsToResolve<V>>(
|
||||
Schema: CoValueClass<V>,
|
||||
id: ID<CoValue> | undefined,
|
||||
depth: D = [] as D
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> }
|
||||
): {
|
||||
current?: DeeplyLoaded<V, D> | null;
|
||||
current: Resolved<V, R> | undefined | null;
|
||||
} {
|
||||
const ctx = getJazzContext<RegisteredAccount>();
|
||||
|
||||
// Create state and a stable observable
|
||||
let state = $state.raw<DeeplyLoaded<V, D> | undefined | null>(undefined);
|
||||
let state = $state.raw<Resolved<V, R> | undefined | null>(undefined);
|
||||
|
||||
// Effect to handle subscription
|
||||
$effect(() => {
|
||||
@@ -205,20 +178,27 @@ export function useCoState<V extends CoValue, D extends DepthsIn<V> = []>(
|
||||
// Return early if no context or id, effectively cleaning up any previous subscription
|
||||
if (!ctx?.current || !id) return;
|
||||
|
||||
const agent = "me" in ctx.current ? ctx.current.me : ctx.current.guest;
|
||||
|
||||
// Setup subscription with current values
|
||||
return subscribeToCoValue(
|
||||
return subscribeToCoValue<V, R>(
|
||||
Schema,
|
||||
id,
|
||||
'me' in ctx.current ? ctx.current.me : ctx.current.guest,
|
||||
depth,
|
||||
{
|
||||
resolve: options?.resolve,
|
||||
loadAs: agent,
|
||||
onUnavailable: () => {
|
||||
state = null;
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
state = null;
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
// Get current value from our stable observable
|
||||
state = value;
|
||||
},
|
||||
() => {
|
||||
state = null;
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ function setup<T extends CoValue>(options: {
|
||||
context: createJazzTestContext({ account: options.account }),
|
||||
props: {
|
||||
invitedObjectSchema: options.invitedObjectSchema,
|
||||
onAccept: (id: ID<T>) => {
|
||||
result.current = id;
|
||||
onAccept: (id: ID<CoValue>) => {
|
||||
result.current = id as ID<T>;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -54,7 +54,9 @@ describe("useAcceptInvite", () => {
|
||||
expect(result.current).toBeDefined();
|
||||
});
|
||||
|
||||
const accepted = await TestMap.load(result.current!, account, {});
|
||||
const accepted = await TestMap.load(result.current!, {
|
||||
loadAs: account,
|
||||
});
|
||||
|
||||
expect(accepted?.value).toEqual("hello");
|
||||
});
|
||||
|
||||
@@ -66,7 +66,9 @@ export class DemoAuth {
|
||||
}
|
||||
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
currentAccount.profile.name = username;
|
||||
|
||||
@@ -83,7 +83,9 @@ export class PassphraseAuth {
|
||||
|
||||
if (name?.trim()) {
|
||||
const currentAccount = await Account.getMe().ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
currentAccount.profile.name = name;
|
||||
|
||||
@@ -20,18 +20,22 @@ import {
|
||||
type CoValue,
|
||||
CoValueBase,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
Ref,
|
||||
type RefEncoded,
|
||||
RefIfCoValue,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
type Schema,
|
||||
SchemaInit,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
ensureCoValueLoaded,
|
||||
inspect,
|
||||
loadCoValue,
|
||||
loadCoValueWithoutMe,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
subscriptionsScopes,
|
||||
@@ -225,7 +229,7 @@ export class Account extends CoValueBase implements CoValue {
|
||||
valueID: ID<V>,
|
||||
inviteSecret: InviteSecret,
|
||||
coValueClass: CoValueClass<V>,
|
||||
) {
|
||||
): Promise<Resolved<V, true> | undefined> {
|
||||
if (!this.isLocalNodeOwner) {
|
||||
throw new Error("Only a controlled account can accept invites");
|
||||
}
|
||||
@@ -235,8 +239,9 @@ export class Account extends CoValueBase implements CoValue {
|
||||
inviteSecret,
|
||||
);
|
||||
|
||||
return loadCoValue(coValueClass, valueID, this as Account, []);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return loadCoValue(coValueClass, valueID, {
|
||||
loadAs: this,
|
||||
});
|
||||
}
|
||||
|
||||
/** @private */
|
||||
@@ -346,73 +351,62 @@ export class Account extends CoValueBase implements CoValue {
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static load<A extends Account, Depth>(
|
||||
static load<A extends Account, const R extends RefsToResolve<A> = true>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
): Promise<DeeplyLoaded<A, Depth> | undefined>;
|
||||
static load<A extends Account, Depth>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
): Promise<DeeplyLoaded<A, Depth> | undefined>;
|
||||
static load<A extends Account, Depth>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<A>),
|
||||
depth?: Depth & DepthsIn<A>,
|
||||
): Promise<DeeplyLoaded<A, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
options?: {
|
||||
resolve?: RefsToResolveStrict<A, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<A, R> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<A extends Account, Depth>(
|
||||
static subscribe<A extends Account, const R extends RefsToResolve<A> = true>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
listener: (value: DeeplyLoaded<A, Depth>) => void,
|
||||
listener: (value: Resolved<A, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<A extends Account, Depth>(
|
||||
static subscribe<A extends Account, const R extends RefsToResolve<A> = true>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
listener: (value: DeeplyLoaded<A, Depth>) => void,
|
||||
options: SubscribeListenerOptions<A, R>,
|
||||
listener: (value: Resolved<A, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<A extends Account, Depth>(
|
||||
static subscribe<A extends Account, const R extends RefsToResolve<A>>(
|
||||
this: CoValueClass<A>,
|
||||
id: ID<A>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<A>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<A>)
|
||||
| ((value: DeeplyLoaded<A, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<A, Depth>) => void,
|
||||
...args: SubscribeRestArgs<A, R>
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<A, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener!,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<A, R>(this, id, options, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<A extends Account, Depth>(
|
||||
ensureLoaded<A extends Account, const R extends RefsToResolve<A>>(
|
||||
this: A,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
): Promise<DeeplyLoaded<A, Depth>> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
options: { resolve: RefsToResolveStrict<A, R> },
|
||||
): Promise<Resolved<A, R>> {
|
||||
return ensureCoValueLoaded(this, options);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
subscribe<A extends Account, Depth>(
|
||||
subscribe<A extends Account, const R extends RefsToResolve<A>>(
|
||||
this: A,
|
||||
depth: Depth & DepthsIn<A>,
|
||||
listener: (value: DeeplyLoaded<A, Depth>) => void,
|
||||
listener: (value: Resolved<A, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<A extends Account, const R extends RefsToResolve<A>>(
|
||||
this: A,
|
||||
options: { resolve?: RefsToResolveStrict<A, R> },
|
||||
listener: (value: Resolved<A, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<A extends Account, const R extends RefsToResolve<A>>(
|
||||
this: A,
|
||||
...args: SubscribeRestArgs<A, R>
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToExistingCoValue(this, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,17 +10,19 @@ import type {
|
||||
SessionID,
|
||||
} from "cojson";
|
||||
import { MAX_RECOMMENDED_TX_SIZE, cojsonInternals } from "cojson";
|
||||
import { activeAccountContext } from "../implementation/activeAccountContext.js";
|
||||
import type {
|
||||
AnonymousJazzAgent,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
IfCo,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
Schema,
|
||||
SchemaFor,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
UnCo,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
@@ -31,11 +33,10 @@ import {
|
||||
co,
|
||||
ensureCoValueLoaded,
|
||||
inspect,
|
||||
isAccountInstance,
|
||||
isRefEncoded,
|
||||
loadCoValueWithoutMe,
|
||||
parseCoValueCreateOptions,
|
||||
subscribeToCoValue,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
@@ -326,73 +327,52 @@ export class CoFeed<Item = any> extends CoValueBase implements CoValue {
|
||||
* Load a `CoFeed`
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static load<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
): Promise<DeeplyLoaded<S, Depth> | undefined>;
|
||||
static load<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
): Promise<DeeplyLoaded<S, Depth> | undefined>;
|
||||
static load<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<S>),
|
||||
depth?: Depth & DepthsIn<S>,
|
||||
): Promise<DeeplyLoaded<S, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
static load<F extends CoFeed, const R extends RefsToResolve<F> = true>(
|
||||
this: CoValueClass<F>,
|
||||
id: ID<F>,
|
||||
options: {
|
||||
resolve?: RefsToResolveStrict<F, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<F, R> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a `CoFeed`, when you have an ID but don't have a `CoFeed` instance yet
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribe<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
listener: (value: DeeplyLoaded<S, Depth>) => void,
|
||||
static subscribe<F extends CoFeed, const R extends RefsToResolve<F> = true>(
|
||||
this: CoValueClass<F>,
|
||||
id: ID<F>,
|
||||
listener: (value: Resolved<F, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
listener: (value: DeeplyLoaded<S, Depth>) => void,
|
||||
static subscribe<F extends CoFeed, const R extends RefsToResolve<F> = true>(
|
||||
this: CoValueClass<F>,
|
||||
id: ID<F>,
|
||||
options: SubscribeListenerOptions<F, R>,
|
||||
listener: (value: Resolved<F, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<S extends CoFeed, Depth>(
|
||||
this: CoValueClass<S>,
|
||||
id: ID<S>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<S>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<S>)
|
||||
| ((value: DeeplyLoaded<S, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<S, Depth>) => void,
|
||||
static subscribe<F extends CoFeed, const R extends RefsToResolve<F>>(
|
||||
this: CoValueClass<F>,
|
||||
id: ID<F>,
|
||||
...args: SubscribeRestArgs<F, R>
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<S, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<F, R>(this, id, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a `CoFeed` is loaded to the specified depth
|
||||
*
|
||||
* @returns A new instance of the same CoFeed that's loaded to the specified depth,
|
||||
* or undefined if it cannot be loaded that deeply
|
||||
* @returns A new instance of the same CoFeed that's loaded to the specified depth
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
ensureLoaded<S extends CoFeed, Depth>(
|
||||
this: S,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
): Promise<DeeplyLoaded<S, Depth>> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
ensureLoaded<F extends CoFeed, const R extends RefsToResolve<F>>(
|
||||
this: F,
|
||||
options?: { resolve?: RefsToResolveStrict<F, R> },
|
||||
): Promise<Resolved<F, R>> {
|
||||
return ensureCoValueLoaded(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,12 +381,21 @@ export class CoFeed<Item = any> extends CoValueBase implements CoValue {
|
||||
* No need to provide an ID or Account since they're already part of the instance.
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
subscribe<S extends CoFeed, Depth>(
|
||||
this: S,
|
||||
depth: Depth & DepthsIn<S>,
|
||||
listener: (value: DeeplyLoaded<S, Depth>) => void,
|
||||
subscribe<F extends CoFeed, const R extends RefsToResolve<F>>(
|
||||
this: F,
|
||||
listener: (value: Resolved<F, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<F extends CoFeed, const R extends RefsToResolve<F>>(
|
||||
this: F,
|
||||
options: { resolve?: RefsToResolveStrict<F, R> },
|
||||
listener: (value: Resolved<F, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<F extends CoFeed, const R extends RefsToResolve<F>>(
|
||||
this: F,
|
||||
...args: SubscribeRestArgs<F, R>
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToExistingCoValue(this, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -784,34 +773,10 @@ export class FileStream extends CoValueBase implements CoValue {
|
||||
id: ID<FileStream>,
|
||||
options?: {
|
||||
allowUnfinished?: boolean;
|
||||
},
|
||||
): Promise<Blob | undefined>;
|
||||
static async loadAsBlob(
|
||||
id: ID<FileStream>,
|
||||
as: Account,
|
||||
options?: {
|
||||
allowUnfinished?: boolean;
|
||||
},
|
||||
): Promise<Blob | undefined>;
|
||||
static async loadAsBlob(
|
||||
id: ID<FileStream>,
|
||||
asOrOptions?:
|
||||
| Account
|
||||
| {
|
||||
allowUnfinished?: boolean;
|
||||
},
|
||||
optionsOrUndefined?: {
|
||||
allowUnfinished?: boolean;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Blob | undefined> {
|
||||
const as = isAccountInstance(asOrOptions)
|
||||
? asOrOptions
|
||||
: activeAccountContext.get();
|
||||
const options = isAccountInstance(asOrOptions)
|
||||
? optionsOrUndefined
|
||||
: asOrOptions;
|
||||
|
||||
let stream = await this.load(id, as, []);
|
||||
let stream = await this.load(id, options);
|
||||
|
||||
/**
|
||||
* If the user hasn't requested an incomplete blob and the
|
||||
@@ -819,12 +784,17 @@ export class FileStream extends CoValueBase implements CoValue {
|
||||
*/
|
||||
if (!options?.allowUnfinished && !stream?.isBinaryStreamEnded()) {
|
||||
stream = await new Promise<FileStream>((resolve) => {
|
||||
subscribeToCoValue(this, id, as, [], (value, unsubscribe) => {
|
||||
if (value.isBinaryStreamEnded()) {
|
||||
unsubscribe();
|
||||
resolve(value);
|
||||
}
|
||||
});
|
||||
subscribeToCoValueWithoutMe(
|
||||
this,
|
||||
id,
|
||||
options || {},
|
||||
(value, unsubscribe) => {
|
||||
if (value.isBinaryStreamEnded()) {
|
||||
unsubscribe();
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -923,78 +893,36 @@ export class FileStream extends CoValueBase implements CoValue {
|
||||
* Load a `FileStream`
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static load<C extends FileStream, Depth>(
|
||||
static load<C extends FileStream>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends FileStream, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends FileStream, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depth?: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
options?: { loadAs?: Account | AnonymousJazzAgent },
|
||||
): Promise<Resolved<C, true> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a `FileStream`, when you have an ID but don't have a `FileStream` instance yet
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribe<C extends FileStream, Depth>(
|
||||
static subscribe<C extends FileStream>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends FileStream, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends FileStream, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<C>)
|
||||
| ((value: DeeplyLoaded<C, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
options: { loadAs?: Account | AnonymousJazzAgent },
|
||||
listener: (value: Resolved<C, true>) => void,
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<C, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener,
|
||||
);
|
||||
}
|
||||
|
||||
ensureLoaded<B extends FileStream, Depth>(
|
||||
this: B,
|
||||
depth: Depth & DepthsIn<B>,
|
||||
): Promise<DeeplyLoaded<B, Depth> | undefined> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
return subscribeToCoValueWithoutMe<C, true>(this, id, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance method to subscribe to an existing `FileStream`
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
subscribe<B extends FileStream, Depth>(
|
||||
subscribe<B extends FileStream>(
|
||||
this: B,
|
||||
depth: Depth & DepthsIn<B>,
|
||||
listener: (value: DeeplyLoaded<B, Depth>) => void,
|
||||
listener: (value: Resolved<B, true>) => void,
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
return subscribeToExistingCoValue(this, {}, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,15 @@ import type {
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
CoValueFromRaw,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
RefEncoded,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
Schema,
|
||||
SchemaFor,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
UnCo,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
@@ -24,6 +27,7 @@ import {
|
||||
loadCoValueWithoutMe,
|
||||
makeRefs,
|
||||
parseCoValueCreateOptions,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
subscriptionsScopes,
|
||||
@@ -360,24 +364,15 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static load<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depth?: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
static load<L extends CoList, const R extends RefsToResolve<L> = true>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
options?: {
|
||||
resolve?: RefsToResolveStrict<L, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<L, R> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,35 +403,24 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribe<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<L extends CoList, const R extends RefsToResolve<L> = true>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
listener: (value: Resolved<L, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<L extends CoList, const R extends RefsToResolve<L> = true>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
options: SubscribeListenerOptions<L, R>,
|
||||
listener: (value: Resolved<L, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends CoList, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<C>)
|
||||
| ((value: DeeplyLoaded<C, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<L extends CoList, const R extends RefsToResolve<L>>(
|
||||
this: CoValueClass<L>,
|
||||
id: ID<L>,
|
||||
...args: SubscribeRestArgs<L, R>
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<C, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<L, R>(this, id, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,11 +430,11 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
ensureLoaded<L extends CoList, Depth>(
|
||||
ensureLoaded<L extends CoList, const R extends RefsToResolve<L>>(
|
||||
this: L,
|
||||
depth: Depth & DepthsIn<L>,
|
||||
): Promise<DeeplyLoaded<L, Depth>> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
options: { resolve: RefsToResolveStrict<L, R> },
|
||||
): Promise<Resolved<L, R>> {
|
||||
return ensureCoValueLoaded(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,12 +446,21 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
**/
|
||||
subscribe<L extends CoList, Depth>(
|
||||
subscribe<L extends CoList, const R extends RefsToResolve<L> = true>(
|
||||
this: L,
|
||||
depth: Depth & DepthsIn<L>,
|
||||
listener: (value: DeeplyLoaded<L, Depth>) => void,
|
||||
listener: (value: Resolved<L, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<L extends CoList, const R extends RefsToResolve<L> = true>(
|
||||
this: L,
|
||||
options: { resolve?: RefsToResolveStrict<L, R> },
|
||||
listener: (value: Resolved<L, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<L extends CoList, const R extends RefsToResolve<L>>(
|
||||
this: L,
|
||||
...args: SubscribeRestArgs<L, R>
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToExistingCoValue(this, options, listener);
|
||||
}
|
||||
|
||||
/** @category Type Helpers */
|
||||
|
||||
@@ -12,13 +12,16 @@ import type {
|
||||
AnonymousJazzAgent,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
IfCo,
|
||||
RefEncoded,
|
||||
RefIfCoValue,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
Schema,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
co,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
loadCoValueWithoutMe,
|
||||
makeRefs,
|
||||
parseCoValueCreateOptions,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
subscriptionsScopes,
|
||||
@@ -432,24 +436,15 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static load<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depth?: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
static load<M extends CoMap, const R extends RefsToResolve<M> = true>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
options?: {
|
||||
resolve?: RefsToResolveStrict<M, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<M, R> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,35 +474,24 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
static subscribe<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<M extends CoMap, const R extends RefsToResolve<M> = true>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
listener: (value: Resolved<M, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<M extends CoMap, const R extends RefsToResolve<M> = true>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
options: SubscribeListenerOptions<M, R>,
|
||||
listener: (value: Resolved<M, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends CoMap, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<C>)
|
||||
| ((value: DeeplyLoaded<C, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<M extends CoMap, const R extends RefsToResolve<M>>(
|
||||
this: CoValueClass<M>,
|
||||
id: ID<M>,
|
||||
...args: SubscribeRestArgs<M, R>
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<C, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<M, R>(this, id, options, listener);
|
||||
}
|
||||
|
||||
static findUnique<M extends CoMap>(
|
||||
@@ -539,11 +523,11 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
*/
|
||||
ensureLoaded<M extends CoMap, Depth>(
|
||||
ensureLoaded<M extends CoMap, const R extends RefsToResolve<M>>(
|
||||
this: M,
|
||||
depth: Depth & DepthsIn<M>,
|
||||
): Promise<DeeplyLoaded<M, Depth>> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
options: { resolve: RefsToResolveStrict<M, R> },
|
||||
): Promise<Resolved<M, R>> {
|
||||
return ensureCoValueLoaded(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -555,12 +539,21 @@ export class CoMap extends CoValueBase implements CoValue {
|
||||
*
|
||||
* @category Subscription & Loading
|
||||
**/
|
||||
subscribe<M extends CoMap, Depth>(
|
||||
subscribe<M extends CoMap, const R extends RefsToResolve<M> = true>(
|
||||
this: M,
|
||||
depth: Depth & DepthsIn<M>,
|
||||
listener: (value: DeeplyLoaded<M, Depth>) => void,
|
||||
listener: (value: Resolved<M, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<M extends CoMap, const R extends RefsToResolve<M> = true>(
|
||||
this: M,
|
||||
options: { resolve?: RefsToResolveStrict<M, R> },
|
||||
listener: (value: Resolved<M, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<M extends CoMap, const R extends RefsToResolve<M>>(
|
||||
this: M,
|
||||
...args: SubscribeRestArgs<M, R>
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToExistingCoValue<M, R>(this, options, listener);
|
||||
}
|
||||
|
||||
applyDiff<N extends Partial<CoMapInit<this>>>(newValues: N) {
|
||||
|
||||
@@ -4,13 +4,20 @@ import {
|
||||
type RawCoPlainText,
|
||||
stringifyOpID,
|
||||
} from "cojson";
|
||||
import { activeAccountContext } from "../implementation/activeAccountContext.js";
|
||||
import type { CoValue, CoValueClass, ID } from "../internal.js";
|
||||
import type {
|
||||
AnonymousJazzAgent,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
ID,
|
||||
Resolved,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
inspect,
|
||||
isAccountInstance,
|
||||
loadCoValue,
|
||||
subscribeToCoValue,
|
||||
loadCoValueWithoutMe,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
import { Account } from "./account.js";
|
||||
@@ -122,9 +129,9 @@ export class CoPlainText extends String implements CoValue {
|
||||
static load<T extends CoPlainText>(
|
||||
this: CoValueClass<T>,
|
||||
id: ID<T>,
|
||||
as?: Account,
|
||||
options?: { loadAs?: Account | AnonymousJazzAgent },
|
||||
): Promise<T | undefined> {
|
||||
return loadCoValue(this, id, as ?? activeAccountContext.get(), []);
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
// /**
|
||||
@@ -157,47 +164,23 @@ export class CoPlainText extends String implements CoValue {
|
||||
static subscribe<T extends CoPlainText>(
|
||||
this: CoValueClass<T>,
|
||||
id: ID<T>,
|
||||
listener: (value: T) => void,
|
||||
listener: (value: Resolved<T, true>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<T extends CoPlainText>(
|
||||
this: CoValueClass<T>,
|
||||
id: ID<T>,
|
||||
as: Account,
|
||||
listener: (value: T) => void,
|
||||
options: Omit<SubscribeListenerOptions<T, true>, "resolve">,
|
||||
listener: (value: Resolved<T, true>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<T extends CoPlainText>(
|
||||
this: CoValueClass<T>,
|
||||
id: ID<T>,
|
||||
asOrListener: Account | ((value: T) => void),
|
||||
listener?: (value: T) => void,
|
||||
...args: SubscribeRestArgs<T, true>
|
||||
): () => void {
|
||||
if (isAccountInstance(asOrListener)) {
|
||||
return subscribeToCoValue(this, id, asOrListener, [], listener!);
|
||||
}
|
||||
|
||||
return subscribeToCoValue(
|
||||
this,
|
||||
id,
|
||||
activeAccountContext.get(),
|
||||
[],
|
||||
listener!,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<T, true>(this, id, options, listener);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Effectful version of `CoMap.subscribe()` that returns a stream of updates.
|
||||
// *
|
||||
// * Needs to be run inside an `AccountCtx` context.
|
||||
// *
|
||||
// * @category Subscription & Loading
|
||||
// */
|
||||
// static subscribeEf<T extends CoPlainText>(
|
||||
// this: CoValueClass<T>,
|
||||
// id: ID<T>,
|
||||
// ): Stream.Stream<T, UnavailableError, AccountCtx> {
|
||||
// return subscribeToCoValueEf(this, id, []);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Given an already loaded `CoPlainText`, subscribe to updates to the `CoPlainText` and ensure that the specified fields are loaded to the specified depth.
|
||||
*
|
||||
@@ -209,8 +192,8 @@ export class CoPlainText extends String implements CoValue {
|
||||
**/
|
||||
subscribe<T extends CoPlainText>(
|
||||
this: T,
|
||||
listener: (value: T) => void,
|
||||
listener: (value: Resolved<T, true>, unsubscribe: () => void) => void,
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, [], listener);
|
||||
return subscribeToExistingCoValue(this, {}, listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,94 +6,208 @@ import { type CoList } from "./coList.js";
|
||||
import { type CoKeys, type CoMap } from "./coMap.js";
|
||||
import { type CoValue, type ID } from "./interfaces.js";
|
||||
|
||||
function hasRefValue(value: CoValue, key: string | number) {
|
||||
return Boolean(
|
||||
(
|
||||
value as unknown as {
|
||||
_refs: { [key: string]: Ref<CoValue> | undefined };
|
||||
}
|
||||
)._refs?.[key],
|
||||
);
|
||||
}
|
||||
|
||||
function hasReadAccess(value: CoValue, key: string | number) {
|
||||
return Boolean(
|
||||
(
|
||||
value as unknown as {
|
||||
_refs: { [key: string]: Ref<CoValue> | undefined };
|
||||
}
|
||||
)._refs?.[key]?.hasReadAccess(),
|
||||
);
|
||||
}
|
||||
|
||||
function isOptionalField(value: CoValue, key: string): boolean {
|
||||
return (
|
||||
((value as CoMap)._schema[key] as RefEncoded<CoValue>)?.optional ?? false
|
||||
);
|
||||
}
|
||||
|
||||
type FulfillsDepthResult = "unauthorized" | "fulfilled" | "unfulfilled";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function fulfillsDepth(depth: any, value: CoValue): boolean {
|
||||
export function fulfillsDepth(depth: any, value: CoValue): FulfillsDepthResult {
|
||||
if (depth === true || depth === undefined) {
|
||||
return "fulfilled";
|
||||
}
|
||||
|
||||
if (
|
||||
value._type === "CoMap" ||
|
||||
value._type === "Group" ||
|
||||
value._type === "Account"
|
||||
) {
|
||||
if (Array.isArray(depth) && depth.length === 1) {
|
||||
return Object.entries(value).every(([key, item]) => {
|
||||
return (
|
||||
value as unknown as {
|
||||
_refs: { [key: string]: Ref<CoValue> | undefined };
|
||||
}
|
||||
)._refs[key]
|
||||
? item && fulfillsDepth(depth[0], item)
|
||||
: ((value as CoMap)._schema[ItemsSym] as RefEncoded<CoValue>)!
|
||||
.optional;
|
||||
});
|
||||
} else {
|
||||
for (const key of Object.keys(depth)) {
|
||||
const map = value as unknown as {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
};
|
||||
const map = value as CoMap;
|
||||
|
||||
if ("$each" in depth) {
|
||||
let result: FulfillsDepthResult = "fulfilled";
|
||||
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (map._raw.get(key) !== undefined) {
|
||||
if (!item) {
|
||||
if (hasReadAccess(map, key)) {
|
||||
result = "unfulfilled";
|
||||
continue;
|
||||
} else {
|
||||
return "unauthorized";
|
||||
}
|
||||
}
|
||||
|
||||
const innerResult = fulfillsDepth(depth.$each, item);
|
||||
|
||||
if (innerResult === "unfulfilled") {
|
||||
result = "unfulfilled";
|
||||
} else if (
|
||||
innerResult === "unauthorized" &&
|
||||
!isOptionalField(value, ItemsSym)
|
||||
) {
|
||||
return "unauthorized"; // If any item is unauthorized, the whole thing is unauthorized
|
||||
}
|
||||
} else if (!isOptionalField(value, ItemsSym)) {
|
||||
return "unfulfilled";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
let result: FulfillsDepthResult = "fulfilled";
|
||||
|
||||
for (const key of Object.keys(depth)) {
|
||||
if (map._raw.get(key) === undefined) {
|
||||
if (!map._schema?.[key]) {
|
||||
// Field not defined in schema
|
||||
if (map._schema?.[ItemsSym]) {
|
||||
// CoMap.Record
|
||||
if (map._schema[ItemsSym].optional) {
|
||||
if (isOptionalField(map, ItemsSym)) {
|
||||
continue;
|
||||
} else {
|
||||
// All fields are required, so the returned type is not optional and we must comply
|
||||
throw new Error(
|
||||
`The ref ${key} requested on ${map.constructor.name} is missing`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Field not defined in CoMap schema
|
||||
throw new Error(
|
||||
`The ref ${key} requested on ${map.constructor.name} is not defined in the schema`,
|
||||
);
|
||||
}
|
||||
} else if (map._schema[key].optional) {
|
||||
} else if (isOptionalField(map, key)) {
|
||||
continue;
|
||||
} else {
|
||||
// Field is required but has never been set
|
||||
throw new Error(
|
||||
`The ref ${key} on ${map.constructor.name} is required but missing`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const item = (value as Record<string, any>)[key];
|
||||
|
||||
if (!map[key]) {
|
||||
return false;
|
||||
}
|
||||
if (!fulfillsDepth(depth[key], map[key])) {
|
||||
return false;
|
||||
if (!item) {
|
||||
if (hasReadAccess(map, key)) {
|
||||
result = "unfulfilled";
|
||||
continue;
|
||||
} else {
|
||||
return "unauthorized";
|
||||
}
|
||||
}
|
||||
|
||||
const innerResult = fulfillsDepth(depth[key], item);
|
||||
|
||||
if (innerResult === "unfulfilled") {
|
||||
result = "unfulfilled";
|
||||
} else if (
|
||||
innerResult === "unauthorized" &&
|
||||
!isOptionalField(value, key)
|
||||
) {
|
||||
return "unauthorized"; // If any item is unauthorized, the whole thing is unauthorized
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return result;
|
||||
}
|
||||
} else if (value._type === "CoList") {
|
||||
if (depth.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
const itemDepth = depth[0];
|
||||
return (value as CoList).every((item, i) =>
|
||||
(value as CoList)._refs[i]
|
||||
? item && fulfillsDepth(itemDepth, item)
|
||||
: ((value as CoList)._schema[ItemsSym] as RefEncoded<CoValue>)
|
||||
.optional,
|
||||
);
|
||||
if ("$each" in depth) {
|
||||
let result: FulfillsDepthResult = "fulfilled";
|
||||
|
||||
for (const [key, item] of (value as CoList).entries()) {
|
||||
if (hasRefValue(value, key)) {
|
||||
if (!item) {
|
||||
if (hasReadAccess(value, key)) {
|
||||
result = "unfulfilled";
|
||||
continue;
|
||||
} else {
|
||||
return "unauthorized";
|
||||
}
|
||||
}
|
||||
|
||||
const innerResult = fulfillsDepth(depth.$each, item);
|
||||
|
||||
if (innerResult === "unfulfilled") {
|
||||
result = "unfulfilled";
|
||||
} else if (
|
||||
innerResult === "unauthorized" &&
|
||||
!isOptionalField(value, ItemsSym)
|
||||
) {
|
||||
return "unauthorized"; // If any item is unauthorized, the whole thing is unauthorized
|
||||
}
|
||||
} else if (!isOptionalField(value, ItemsSym)) {
|
||||
return "unfulfilled";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return "fulfilled";
|
||||
} else if (value._type === "CoStream") {
|
||||
if (depth.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
const itemDepth = depth[0];
|
||||
return Object.values((value as CoFeed).perSession).every((entry) =>
|
||||
entry.ref
|
||||
? entry.value && fulfillsDepth(itemDepth, entry.value)
|
||||
: ((value as CoFeed)._schema[ItemsSym] as RefEncoded<CoValue>)
|
||||
.optional,
|
||||
);
|
||||
if ("$each" in depth) {
|
||||
let result: FulfillsDepthResult = "fulfilled";
|
||||
|
||||
for (const item of Object.values((value as CoFeed).perSession)) {
|
||||
if (item.ref) {
|
||||
if (!item.value) {
|
||||
if (item.ref.hasReadAccess()) {
|
||||
result = "unfulfilled";
|
||||
continue;
|
||||
} else {
|
||||
return "unauthorized";
|
||||
}
|
||||
}
|
||||
|
||||
const innerResult = fulfillsDepth(depth.$each, item.value);
|
||||
|
||||
if (innerResult === "unfulfilled") {
|
||||
result = "unfulfilled";
|
||||
} else if (
|
||||
innerResult === "unauthorized" &&
|
||||
!isOptionalField(value, ItemsSym)
|
||||
) {
|
||||
return "unauthorized"; // If any item is unauthorized, the whole thing is unauthorized
|
||||
}
|
||||
} else if (!isOptionalField(value, ItemsSym)) {
|
||||
return "unfulfilled";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return "fulfilled";
|
||||
} else if (
|
||||
value._type === "BinaryCoStream" ||
|
||||
value._type === "CoPlainText"
|
||||
) {
|
||||
return true;
|
||||
return "fulfilled";
|
||||
} else {
|
||||
console.error(value);
|
||||
throw new Error("Unexpected value type: " + value._type);
|
||||
@@ -101,58 +215,75 @@ export function fulfillsDepth(depth: any, value: CoValue): boolean {
|
||||
}
|
||||
|
||||
type UnCoNotNull<T> = UnCo<Exclude<T, null>>;
|
||||
type Clean<T> = UnCo<NonNullable<T>>;
|
||||
export type Clean<T> = UnCo<NonNullable<T>>;
|
||||
|
||||
export type DepthsIn<
|
||||
export type RefsToResolve<
|
||||
V,
|
||||
DepthLimit extends number = 5,
|
||||
CurrentDepth extends number[] = [],
|
||||
> =
|
||||
| boolean
|
||||
| (DepthLimit extends CurrentDepth["length"]
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any
|
||||
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
|
||||
V extends Array<infer Item>
|
||||
?
|
||||
| [DepthsIn<UnCoNotNull<Item>, DepthLimit, [0, ...CurrentDepth]>]
|
||||
| never[]
|
||||
| {
|
||||
$each: RefsToResolve<
|
||||
UnCoNotNull<Item>,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>;
|
||||
}
|
||||
| boolean
|
||||
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
|
||||
V extends { _type: "CoMap" | "Group" | "Account" }
|
||||
?
|
||||
| {
|
||||
[Key in CoKeys<V> as Clean<V[Key]> extends CoValue
|
||||
? Key
|
||||
: never]?: DepthsIn<
|
||||
: never]?: RefsToResolve<
|
||||
Clean<V[Key]>,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>;
|
||||
}
|
||||
| (ItemsSym extends keyof V
|
||||
? [
|
||||
DepthsIn<
|
||||
? {
|
||||
$each: RefsToResolve<
|
||||
Clean<V[ItemsSym]>,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>,
|
||||
]
|
||||
>;
|
||||
}
|
||||
: never)
|
||||
| never[]
|
||||
| boolean
|
||||
: V extends {
|
||||
_type: "CoStream";
|
||||
byMe: CoFeedEntry<infer Item> | undefined;
|
||||
}
|
||||
?
|
||||
| [
|
||||
DepthsIn<
|
||||
| {
|
||||
$each: RefsToResolve<
|
||||
UnCoNotNull<Item>,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>,
|
||||
]
|
||||
| never[]
|
||||
: never[])
|
||||
| never[];
|
||||
>;
|
||||
}
|
||||
| boolean
|
||||
: boolean);
|
||||
|
||||
export type RefsToResolveStrict<T, V> = V extends RefsToResolve<T>
|
||||
? RefsToResolve<T>
|
||||
: V;
|
||||
|
||||
export type Resolved<T, R extends RefsToResolve<T> | undefined> = DeeplyLoaded<
|
||||
T,
|
||||
R,
|
||||
5,
|
||||
[]
|
||||
>;
|
||||
|
||||
export type DeeplyLoaded<
|
||||
V,
|
||||
@@ -161,41 +292,42 @@ export type DeeplyLoaded<
|
||||
CurrentDepth extends number[] = [],
|
||||
> = DepthLimit extends CurrentDepth["length"]
|
||||
? V
|
||||
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
|
||||
[V] extends [Array<infer Item>]
|
||||
? Depth extends never[] // []
|
||||
? V
|
||||
: UnCoNotNull<Item> extends CoValue
|
||||
? Depth extends Array<infer ItemDepth> // [item-depth]
|
||||
? (UnCoNotNull<Item> &
|
||||
: Depth extends boolean | undefined // Checking against boolean instead of true because the inference from RefsToResolveStrict transforms true into boolean
|
||||
? V
|
||||
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
|
||||
[V] extends [Array<infer Item>]
|
||||
? UnCoNotNull<Item> extends CoValue
|
||||
? Depth extends { $each: infer ItemDepth }
|
||||
? // Deeply loaded CoList
|
||||
(UnCoNotNull<Item> &
|
||||
DeeplyLoaded<
|
||||
UnCoNotNull<Item>,
|
||||
ItemDepth,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>)[] &
|
||||
V
|
||||
V // the CoList base type needs to be intersected after so that built-in methods return the correct narrowed array type
|
||||
: never
|
||||
: V
|
||||
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
|
||||
[V] extends [{ _type: "CoMap" | "Group" | "Account" }]
|
||||
? Depth extends never[]
|
||||
? V
|
||||
: Depth extends Array<infer ItemDepth>
|
||||
? ItemsSym extends keyof V
|
||||
? V & {
|
||||
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
|
||||
[V] extends [{ _type: "CoMap" | "Group" | "Account" }]
|
||||
? ItemsSym extends keyof V
|
||||
? Depth extends { $each: infer ItemDepth }
|
||||
? // Deeply loaded Record-like CoMap
|
||||
{
|
||||
[key: string]: DeeplyLoaded<
|
||||
Clean<V[ItemsSym]>,
|
||||
ItemDepth,
|
||||
DepthLimit,
|
||||
[0, ...CurrentDepth]
|
||||
>;
|
||||
}
|
||||
} & V // same reason as in CoList
|
||||
: never
|
||||
: keyof Depth extends never
|
||||
: keyof Depth extends never // Depth = {}
|
||||
? V
|
||||
: {
|
||||
[Key in keyof Depth]-?: Key extends CoKeys<V>
|
||||
: // Deeply loaded CoMap
|
||||
{
|
||||
-readonly [Key in keyof Depth]-?: Key extends CoKeys<V>
|
||||
? Clean<V[Key]> extends CoValue
|
||||
?
|
||||
| DeeplyLoaded<
|
||||
@@ -207,32 +339,31 @@ export type DeeplyLoaded<
|
||||
| (undefined extends V[Key] ? undefined : never)
|
||||
: never
|
||||
: never;
|
||||
} & V
|
||||
: [V] extends [
|
||||
} & V // same reason as in CoList
|
||||
: [V] extends [
|
||||
{
|
||||
_type: "CoStream";
|
||||
byMe: CoFeedEntry<infer Item> | undefined;
|
||||
},
|
||||
]
|
||||
? // Deeply loaded CoStream
|
||||
{
|
||||
_type: "CoStream";
|
||||
byMe: CoFeedEntry<infer Item> | undefined;
|
||||
},
|
||||
]
|
||||
? Depth extends never[]
|
||||
? V
|
||||
: V & {
|
||||
byMe?: { value: UnCoNotNull<Item> };
|
||||
inCurrentSession?: { value: UnCoNotNull<Item> };
|
||||
perSession: {
|
||||
[key: SessionID]: { value: UnCoNotNull<Item> };
|
||||
};
|
||||
} & { [key: ID<Account>]: { value: UnCoNotNull<Item> } }
|
||||
: [V] extends [
|
||||
{
|
||||
_type: "BinaryCoStream";
|
||||
},
|
||||
]
|
||||
? V
|
||||
} & { [key: ID<Account>]: { value: UnCoNotNull<Item> } } & V // same reason as in CoList
|
||||
: [V] extends [
|
||||
{
|
||||
_type: "CoPlainText";
|
||||
_type: "BinaryCoStream";
|
||||
},
|
||||
]
|
||||
? V
|
||||
: never;
|
||||
: [V] extends [
|
||||
{
|
||||
_type: "CoPlainText";
|
||||
},
|
||||
]
|
||||
? V
|
||||
: never;
|
||||
|
||||
@@ -10,11 +10,14 @@ import { activeAccountContext } from "../implementation/activeAccountContext.js"
|
||||
import type {
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
RefEncoded,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
Schema,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
CoValueBase,
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
ensureCoValueLoaded,
|
||||
loadCoValueWithoutMe,
|
||||
parseGroupCreateOptions,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
@@ -229,73 +233,59 @@ export class Group extends CoValueBase implements CoValue {
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static load<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined>;
|
||||
static load<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depth?: Depth & DepthsIn<C>,
|
||||
): Promise<DeeplyLoaded<C, Depth> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, asOrDepth, depth);
|
||||
static load<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
options?: { resolve?: RefsToResolveStrict<G, R>; loadAs?: Account },
|
||||
): Promise<Resolved<G, R> | undefined> {
|
||||
return loadCoValueWithoutMe(this, id, options);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static subscribe<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
listener: (value: Resolved<G, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
as: Account,
|
||||
depth: Depth & DepthsIn<C>,
|
||||
listener: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
options: SubscribeListenerOptions<G, R>,
|
||||
listener: (value: Resolved<G, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
static subscribe<C extends Group, Depth>(
|
||||
this: CoValueClass<C>,
|
||||
id: ID<C>,
|
||||
asOrDepth: Account | (Depth & DepthsIn<C>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<C>)
|
||||
| ((value: DeeplyLoaded<C, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<C, Depth>) => void,
|
||||
static subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: CoValueClass<G>,
|
||||
id: ID<G>,
|
||||
...args: SubscribeRestArgs<G, R>
|
||||
): () => void {
|
||||
return subscribeToCoValueWithoutMe<C, Depth>(
|
||||
this,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener,
|
||||
);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToCoValueWithoutMe<G, R>(this, id, options, listener);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
ensureLoaded<G extends Group, Depth>(
|
||||
ensureLoaded<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: G,
|
||||
depth: Depth & DepthsIn<G>,
|
||||
): Promise<DeeplyLoaded<G, Depth>> {
|
||||
return ensureCoValueLoaded(this, depth);
|
||||
options?: { resolve?: RefsToResolveStrict<G, R> },
|
||||
): Promise<Resolved<G, R>> {
|
||||
return ensureCoValueLoaded(this, options);
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
subscribe<G extends Group, Depth>(
|
||||
subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: G,
|
||||
depth: Depth & DepthsIn<G>,
|
||||
listener: (value: DeeplyLoaded<G, Depth>) => void,
|
||||
listener: (value: Resolved<G, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: G,
|
||||
options: { resolve?: RefsToResolveStrict<G, R> },
|
||||
listener: (value: Resolved<G, R>, unsubscribe: () => void) => void,
|
||||
): () => void;
|
||||
subscribe<G extends Group, const R extends RefsToResolve<G>>(
|
||||
this: G,
|
||||
...args: SubscribeRestArgs<G, R>
|
||||
): () => void {
|
||||
return subscribeToExistingCoValue(this, depth, listener);
|
||||
const { options, listener } = parseSubscribeRestArgs(args);
|
||||
return subscribeToExistingCoValue(this, options, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -167,12 +167,9 @@ export class Inbox {
|
||||
);
|
||||
}
|
||||
|
||||
return loadCoValue(
|
||||
Schema,
|
||||
message.get("payload") as ID<I>,
|
||||
account,
|
||||
[],
|
||||
);
|
||||
return loadCoValue(Schema, message.get("payload") as ID<I>, {
|
||||
loadAs: account,
|
||||
});
|
||||
})
|
||||
.then((value) => {
|
||||
if (!value) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
import { RawAccount } from "cojson";
|
||||
import { activeAccountContext } from "../implementation/activeAccountContext.js";
|
||||
import { AnonymousJazzAgent } from "../implementation/anonymousJazzAgent.js";
|
||||
import type { DeeplyLoaded, DepthsIn } from "../internal.js";
|
||||
import {
|
||||
Ref,
|
||||
SubscriptionScope,
|
||||
@@ -15,7 +14,12 @@ import {
|
||||
} from "../internal.js";
|
||||
import { coValuesCache } from "../lib/cache.js";
|
||||
import { type Account } from "./account.js";
|
||||
import { fulfillsDepth } from "./deepLoading.js";
|
||||
import {
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
fulfillsDepth,
|
||||
} from "./deepLoading.js";
|
||||
import { type Group } from "./group.js";
|
||||
import { RegisteredSchemas } from "./registeredSchemas.js";
|
||||
|
||||
@@ -155,56 +159,70 @@ export class CoValueBase implements CoValue {
|
||||
}
|
||||
}
|
||||
|
||||
export function loadCoValueWithoutMe<V extends CoValue, Depth>(
|
||||
export function loadCoValueWithoutMe<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<CoValue>,
|
||||
asOrDepth: Account | AnonymousJazzAgent | (Depth & DepthsIn<V>),
|
||||
depth?: Depth & DepthsIn<V>,
|
||||
): Promise<DeeplyLoaded<V, Depth> | undefined> {
|
||||
if (isAccountInstance(asOrDepth) || isAnonymousAgentInstance(asOrDepth)) {
|
||||
if (!depth) {
|
||||
throw new Error(
|
||||
"Depth is required when loading a CoValue as an Account or AnonymousJazzAgent",
|
||||
);
|
||||
}
|
||||
return loadCoValue(cls, id, asOrDepth, depth);
|
||||
}
|
||||
|
||||
return loadCoValue(cls, id, activeAccountContext.get(), asOrDepth);
|
||||
options?: {
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<V, R> | undefined> {
|
||||
return loadCoValue(cls, id, {
|
||||
...options,
|
||||
loadAs: options?.loadAs ?? activeAccountContext.get(),
|
||||
});
|
||||
}
|
||||
|
||||
export function loadCoValue<V extends CoValue, Depth>(
|
||||
export function loadCoValue<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<CoValue>,
|
||||
as: Account | AnonymousJazzAgent,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
): Promise<DeeplyLoaded<V, Depth> | undefined> {
|
||||
options: {
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
loadAs: Account | AnonymousJazzAgent;
|
||||
},
|
||||
): Promise<Resolved<V, R> | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
subscribeToCoValue(
|
||||
subscribeToCoValue<V, R>(
|
||||
cls,
|
||||
id,
|
||||
as,
|
||||
depth,
|
||||
{
|
||||
resolve: options.resolve,
|
||||
loadAs: options.loadAs,
|
||||
onUnavailable: () => {
|
||||
resolve(undefined);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
resolve(undefined);
|
||||
},
|
||||
},
|
||||
(value, unsubscribe) => {
|
||||
resolve(value);
|
||||
unsubscribe();
|
||||
},
|
||||
() => {
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureCoValueLoaded<V extends CoValue, Depth>(
|
||||
export async function ensureCoValueLoaded<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
existing: V,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
): Promise<DeeplyLoaded<V, Depth>> {
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> } | undefined,
|
||||
): Promise<Resolved<V, R>> {
|
||||
const response = await loadCoValue(
|
||||
existing.constructor as CoValueClass<V>,
|
||||
existing.id,
|
||||
existing._loadedAs,
|
||||
depth,
|
||||
{
|
||||
loadAs: existing._loadedAs,
|
||||
resolve: options?.resolve,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
@@ -214,70 +232,140 @@ export async function ensureCoValueLoaded<V extends CoValue, Depth>(
|
||||
return response;
|
||||
}
|
||||
|
||||
export function subscribeToCoValueWithoutMe<V extends CoValue, Depth>(
|
||||
type SubscribeListener<V extends CoValue, R extends RefsToResolve<V>> = (
|
||||
value: Resolved<V, R>,
|
||||
unsubscribe: () => void,
|
||||
) => void;
|
||||
|
||||
export type SubscribeListenerOptions<
|
||||
V extends CoValue,
|
||||
R extends RefsToResolve<V>,
|
||||
> = {
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
loadAs?: Account | AnonymousJazzAgent;
|
||||
onUnauthorized?: () => void;
|
||||
onUnavailable?: () => void;
|
||||
};
|
||||
|
||||
export type SubscribeRestArgs<V extends CoValue, R extends RefsToResolve<V>> =
|
||||
| [options: SubscribeListenerOptions<V, R>, listener: SubscribeListener<V, R>]
|
||||
| [listener: SubscribeListener<V, R>];
|
||||
|
||||
export function parseSubscribeRestArgs<
|
||||
V extends CoValue,
|
||||
R extends RefsToResolve<V>,
|
||||
>(
|
||||
args: SubscribeRestArgs<V, R>,
|
||||
): {
|
||||
options: SubscribeListenerOptions<V, R>;
|
||||
listener: SubscribeListener<V, R>;
|
||||
} {
|
||||
if (args.length === 2) {
|
||||
if (
|
||||
typeof args[0] === "object" &&
|
||||
args[0] &&
|
||||
typeof args[1] === "function"
|
||||
) {
|
||||
return {
|
||||
options: {
|
||||
resolve: args[0].resolve,
|
||||
loadAs: args[0].loadAs,
|
||||
onUnauthorized: args[0].onUnauthorized,
|
||||
onUnavailable: args[0].onUnavailable,
|
||||
},
|
||||
listener: args[1],
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
} else {
|
||||
if (typeof args[0] === "function") {
|
||||
return { options: {}, listener: args[0] };
|
||||
} else {
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToCoValueWithoutMe<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<CoValue>,
|
||||
asOrDepth: Account | AnonymousJazzAgent | (Depth & DepthsIn<V>),
|
||||
depthOrListener:
|
||||
| (Depth & DepthsIn<V>)
|
||||
| ((value: DeeplyLoaded<V, Depth>) => void),
|
||||
listener?: (value: DeeplyLoaded<V, Depth>) => void,
|
||||
options: SubscribeListenerOptions<V, R>,
|
||||
listener: SubscribeListener<V, R>,
|
||||
) {
|
||||
if (isAccountInstance(asOrDepth) || isAnonymousAgentInstance(asOrDepth)) {
|
||||
if (typeof depthOrListener !== "function") {
|
||||
return subscribeToCoValue<V, Depth>(
|
||||
cls,
|
||||
id,
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
listener!,
|
||||
);
|
||||
}
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
|
||||
if (typeof depthOrListener !== "function") {
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
|
||||
return subscribeToCoValue<V, Depth>(
|
||||
return subscribeToCoValue(
|
||||
cls,
|
||||
id,
|
||||
activeAccountContext.get(),
|
||||
asOrDepth,
|
||||
depthOrListener,
|
||||
{
|
||||
...options,
|
||||
loadAs: options.loadAs ?? activeAccountContext.get(),
|
||||
},
|
||||
listener,
|
||||
);
|
||||
}
|
||||
|
||||
export function subscribeToCoValue<V extends CoValue, Depth>(
|
||||
export function subscribeToCoValue<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<CoValue>,
|
||||
as: Account | AnonymousJazzAgent,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
listener: (value: DeeplyLoaded<V, Depth>, unsubscribe: () => void) => void,
|
||||
onUnavailable?: () => void,
|
||||
syncResolution?: boolean,
|
||||
options: {
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
loadAs: Account | AnonymousJazzAgent;
|
||||
onUnavailable?: () => void;
|
||||
onUnauthorized?: () => void;
|
||||
syncResolution?: boolean;
|
||||
},
|
||||
listener: SubscribeListener<V, R>,
|
||||
): () => void {
|
||||
const ref = new Ref(id, as, { ref: cls, optional: false });
|
||||
const ref = new Ref(id, options.loadAs, { ref: cls, optional: false });
|
||||
|
||||
let unsubscribed = false;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
function subscribe(value: CoValue | undefined) {
|
||||
function subscribe() {
|
||||
const value = ref.getValueWithoutAccessCheck();
|
||||
|
||||
if (!value) {
|
||||
onUnavailable && onUnavailable();
|
||||
options.onUnavailable?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (unsubscribed) return;
|
||||
const subscription = new SubscriptionScope(
|
||||
value,
|
||||
cls as CoValueClass<V> & CoValueFromRaw<V>,
|
||||
(update, subscription) => {
|
||||
if (fulfillsDepth(depth, update)) {
|
||||
listener(
|
||||
update as DeeplyLoaded<V, Depth>,
|
||||
subscription.unsubscribeAll,
|
||||
if (!ref.hasReadAccess()) {
|
||||
options.onUnauthorized?.();
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = fulfillsDepth(options.resolve, update);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Failed to load / subscribe to CoValue",
|
||||
e,
|
||||
e instanceof Error ? e.stack : undefined,
|
||||
);
|
||||
options.onUnavailable?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === "unauthorized") {
|
||||
options.onUnauthorized?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === "fulfilled") {
|
||||
listener(update as Resolved<V, R>, subscription.unsubscribeAll);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -285,17 +373,21 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
|
||||
unsubscribe = subscription.unsubscribeAll;
|
||||
}
|
||||
|
||||
const sync = syncResolution ? ref.syncLoad() : undefined;
|
||||
const sync = options.syncResolution ? ref.syncLoad() : undefined;
|
||||
|
||||
if (sync) {
|
||||
subscribe(sync);
|
||||
subscribe();
|
||||
} else {
|
||||
ref
|
||||
.load()
|
||||
.then((value) => subscribe(value))
|
||||
.then(() => subscribe())
|
||||
.catch((e) => {
|
||||
console.error("Failed to load / subscribe to CoValue", e);
|
||||
onUnavailable?.();
|
||||
console.error(
|
||||
"Failed to load / subscribe to CoValue",
|
||||
e,
|
||||
e instanceof Error ? e.stack : undefined,
|
||||
);
|
||||
options.onUnavailable?.();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -305,36 +397,47 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
|
||||
};
|
||||
}
|
||||
|
||||
export function createCoValueObservable<V extends CoValue, Depth>(options?: {
|
||||
syncResolution?: boolean;
|
||||
}) {
|
||||
let currentValue: DeeplyLoaded<V, Depth> | undefined | null = undefined;
|
||||
export function createCoValueObservable<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>() {
|
||||
let currentValue: Resolved<V, R> | undefined | null = undefined;
|
||||
let subscriberCount = 0;
|
||||
|
||||
function subscribe(
|
||||
cls: CoValueClass<V>,
|
||||
id: ID<CoValue>,
|
||||
as: Account | AnonymousJazzAgent,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
options: {
|
||||
loadAs: Account | AnonymousJazzAgent;
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
onUnavailable?: () => void;
|
||||
onUnauthorized?: () => void;
|
||||
syncResolution?: boolean;
|
||||
},
|
||||
listener: () => void,
|
||||
onUnavailable?: () => void,
|
||||
) {
|
||||
subscriberCount++;
|
||||
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
cls,
|
||||
id,
|
||||
as,
|
||||
depth,
|
||||
{
|
||||
loadAs: options.loadAs,
|
||||
resolve: options.resolve,
|
||||
onUnavailable: () => {
|
||||
currentValue = null;
|
||||
options.onUnavailable?.();
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
currentValue = null;
|
||||
options.onUnauthorized?.();
|
||||
},
|
||||
syncResolution: options.syncResolution,
|
||||
},
|
||||
(value) => {
|
||||
currentValue = value;
|
||||
listener();
|
||||
},
|
||||
() => {
|
||||
currentValue = null;
|
||||
onUnavailable?.();
|
||||
},
|
||||
options?.syncResolution,
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -354,16 +457,29 @@ export function createCoValueObservable<V extends CoValue, Depth>(options?: {
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function subscribeToExistingCoValue<V extends CoValue, Depth>(
|
||||
export function subscribeToExistingCoValue<
|
||||
V extends CoValue,
|
||||
const R extends RefsToResolve<V>,
|
||||
>(
|
||||
existing: V,
|
||||
depth: Depth & DepthsIn<V>,
|
||||
listener: (value: DeeplyLoaded<V, Depth>) => void,
|
||||
options:
|
||||
| {
|
||||
resolve?: RefsToResolveStrict<V, R>;
|
||||
onUnavailable?: () => void;
|
||||
onUnauthorized?: () => void;
|
||||
}
|
||||
| undefined,
|
||||
listener: SubscribeListener<V, R>,
|
||||
): () => void {
|
||||
return subscribeToCoValue(
|
||||
existing.constructor as CoValueClass<V>,
|
||||
existing.id,
|
||||
existing._loadedAs,
|
||||
depth,
|
||||
{
|
||||
loadAs: existing._loadedAs,
|
||||
resolve: options?.resolve,
|
||||
onUnavailable: options?.onUnavailable,
|
||||
onUnauthorized: options?.onUnauthorized,
|
||||
},
|
||||
listener,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,13 @@ export { CoValueBase } from "./coValues/interfaces.js";
|
||||
export { Profile } from "./coValues/profile.js";
|
||||
export { SchemaUnion } from "./coValues/schemaUnion.js";
|
||||
|
||||
export type { CoValueClass, DeeplyLoaded, DepthsIn } from "./internal.js";
|
||||
export type {
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
Resolved,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
} from "./internal.js";
|
||||
|
||||
export {
|
||||
createCoValueObservable,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CoID, RawCoValue } from "cojson";
|
||||
import { type CoID, RawAccount, type RawCoValue, RawGroup } from "cojson";
|
||||
import { type Account } from "../coValues/account.js";
|
||||
import type {
|
||||
AnonymousJazzAgent,
|
||||
@@ -27,12 +27,42 @@ export class Ref<out V extends CoValue> {
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
const node =
|
||||
"node" in this.controlledAccount
|
||||
? this.controlledAccount.node
|
||||
: this.controlledAccount._raw.core.node;
|
||||
private getNode() {
|
||||
return "node" in this.controlledAccount
|
||||
? this.controlledAccount.node
|
||||
: this.controlledAccount._raw.core.node;
|
||||
}
|
||||
|
||||
hasReadAccess() {
|
||||
const node = this.getNode();
|
||||
|
||||
const raw = node.getLoaded(this.id as unknown as CoID<RawCoValue>);
|
||||
|
||||
if (!raw) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (raw instanceof RawAccount || raw instanceof RawGroup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const group = raw.core.getGroup();
|
||||
|
||||
if (group instanceof RawAccount) {
|
||||
if (node.account.id !== group.id) {
|
||||
return false;
|
||||
}
|
||||
} else if (group.myRole() === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getValueWithoutAccessCheck() {
|
||||
const node = this.getNode();
|
||||
const raw = node.getLoaded(this.id as unknown as CoID<RawCoValue>);
|
||||
|
||||
if (raw) {
|
||||
return coValuesCache.get(raw, () =>
|
||||
instantiateRefEncoded(this.schema, raw),
|
||||
@@ -42,6 +72,14 @@ export class Ref<out V extends CoValue> {
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (!this.hasReadAccess()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getValueWithoutAccessCheck();
|
||||
}
|
||||
|
||||
private async loadHelper(): Promise<V | "unavailable"> {
|
||||
const node =
|
||||
"node" in this.controlledAccount
|
||||
@@ -100,7 +138,7 @@ export class Ref<out V extends CoValue> {
|
||||
} else if (this.value !== null) {
|
||||
const freshValueInstance = instantiateRefEncoded(
|
||||
this.schema,
|
||||
this.value?._raw,
|
||||
this.value._raw,
|
||||
);
|
||||
TRACE_ACCESSES && console.log("freshValueInstance", freshValueInstance);
|
||||
subScope.cachedValues[this.id] = freshValueInstance;
|
||||
|
||||
@@ -247,7 +247,9 @@ describe("ContextManager", () => {
|
||||
provider: "test",
|
||||
});
|
||||
|
||||
const me = await CustomAccount.getMe().ensureLoaded({ root: {} });
|
||||
const me = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
expect(me.root.id).toBe(lastRootId);
|
||||
});
|
||||
@@ -266,7 +268,7 @@ describe("ContextManager", () => {
|
||||
value: 1,
|
||||
});
|
||||
} else {
|
||||
const { root } = await this.ensureLoaded({ root: {} });
|
||||
const { root } = await this.ensureLoaded({ resolve: { root: true } });
|
||||
|
||||
root.value = 2;
|
||||
}
|
||||
@@ -289,7 +291,9 @@ describe("ContextManager", () => {
|
||||
provider: "test",
|
||||
});
|
||||
|
||||
const me = await CustomAccount.getMe().ensureLoaded({ root: {} });
|
||||
const me = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
expect(me.root.value).toBe(2);
|
||||
});
|
||||
@@ -316,10 +320,16 @@ describe("ContextManager", () => {
|
||||
anonymousAccount: CustomAccount,
|
||||
) => {
|
||||
const anonymousAccountWithRoot = await anonymousAccount.ensureLoaded({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
});
|
||||
|
||||
const meWithRoot = await CustomAccount.getMe().ensureLoaded({ root: {} });
|
||||
const meWithRoot = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
});
|
||||
|
||||
const rootToTransfer = anonymousAccountWithRoot.root;
|
||||
|
||||
@@ -347,7 +357,11 @@ describe("ContextManager", () => {
|
||||
provider: "test",
|
||||
});
|
||||
|
||||
const me = await CustomAccount.getMe().ensureLoaded({ root: {} });
|
||||
const me = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(me.root.transferredRoot?.value).toBe("Hello");
|
||||
});
|
||||
|
||||
@@ -152,7 +152,9 @@ describe("PassphraseAuth", () => {
|
||||
|
||||
// Verify the account name was set
|
||||
const { profile } = await account.ensureLoaded({
|
||||
profile: {},
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
expect(profile.name).toBe(testName);
|
||||
|
||||
|
||||
@@ -124,15 +124,16 @@ describe("CoFeed resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const loadedStream = await TestStream.load(stream.id, meOnSecondPeer, []);
|
||||
const loadedStream = await TestStream.load(stream.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
});
|
||||
|
||||
expect(loadedStream?.[me.id]?.value).toEqual(null);
|
||||
expect(loadedStream?.[me.id]?.ref?.id).toEqual(stream[me.id]?.value?.id);
|
||||
|
||||
const loadedNestedStream = await NestedStream.load(
|
||||
stream[me.id]!.value!.id,
|
||||
meOnSecondPeer,
|
||||
[],
|
||||
{ loadAs: meOnSecondPeer },
|
||||
);
|
||||
|
||||
// expect(loadedStream?.[me.id]?.value).toEqual(loadedNestedStream);
|
||||
@@ -148,8 +149,7 @@ describe("CoFeed resolution", async () => {
|
||||
|
||||
const loadedTwiceNestedStream = await TwiceNestedStream.load(
|
||||
stream[me.id]!.value![me.id]!.value!.id,
|
||||
meOnSecondPeer,
|
||||
[],
|
||||
{ loadAs: meOnSecondPeer },
|
||||
);
|
||||
|
||||
// expect(loadedStream?.[me.id]?.value?.[me.id]?.value).toEqual(
|
||||
@@ -212,9 +212,13 @@ describe("CoFeed resolution", async () => {
|
||||
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
TestStream.subscribe(stream.id, meOnSecondPeer, [], (subscribedStream) => {
|
||||
void queue.push(subscribedStream);
|
||||
});
|
||||
TestStream.subscribe(
|
||||
stream.id,
|
||||
{ loadAs: meOnSecondPeer },
|
||||
(subscribedStream) => {
|
||||
void queue.push(subscribedStream);
|
||||
},
|
||||
);
|
||||
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1[me.id]?.value).toEqual(null);
|
||||
@@ -330,7 +334,9 @@ describe("FileStream loading & Subscription", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const loadedStream = await FileStream.load(stream.id, meOnSecondPeer, []);
|
||||
const loadedStream = await FileStream.load(stream.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
});
|
||||
|
||||
expect(loadedStream?.getChunks()).toEqual({
|
||||
mimeType: "text/plain",
|
||||
@@ -364,9 +370,13 @@ describe("FileStream loading & Subscription", async () => {
|
||||
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
FileStream.subscribe(stream.id, meOnSecondPeer, [], (subscribedStream) => {
|
||||
void queue.push(subscribedStream);
|
||||
});
|
||||
FileStream.subscribe(
|
||||
stream.id,
|
||||
{ loadAs: meOnSecondPeer },
|
||||
(subscribedStream) => {
|
||||
void queue.push(subscribedStream);
|
||||
},
|
||||
);
|
||||
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1.getChunks()).toBe(undefined);
|
||||
@@ -435,9 +445,7 @@ describe("FileStream.loadAsBlob", async () => {
|
||||
const { stream, me } = await setup();
|
||||
stream.push(new Uint8Array([1]));
|
||||
|
||||
const promise = FileStream.loadAsBlob(stream.id, me);
|
||||
|
||||
await stream.ensureLoaded([]);
|
||||
const promise = FileStream.loadAsBlob(stream.id, { loadAs: me });
|
||||
|
||||
stream.push(new Uint8Array([2]));
|
||||
stream.end();
|
||||
@@ -453,17 +461,16 @@ describe("FileStream.loadAsBlob", async () => {
|
||||
const { stream, me } = await setup();
|
||||
stream.push(new Uint8Array([1]));
|
||||
|
||||
const promise = FileStream.loadAsBlob(stream.id, me, {
|
||||
const promise = FileStream.loadAsBlob(stream.id, {
|
||||
loadAs: me,
|
||||
allowUnfinished: true,
|
||||
});
|
||||
|
||||
await stream.ensureLoaded([]);
|
||||
const blob = await promise;
|
||||
|
||||
stream.push(new Uint8Array([2]));
|
||||
stream.end();
|
||||
|
||||
const blob = await promise;
|
||||
|
||||
// The promise resolves before the stream is ended
|
||||
// so we get a blob only with the first chunk
|
||||
expect(blob?.size).toBe(1);
|
||||
|
||||
@@ -181,16 +181,14 @@ describe("CoList resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const loadedList = await TestList.load(list.id, meOnSecondPeer, []);
|
||||
const loadedList = await TestList.load(list.id, { loadAs: meOnSecondPeer });
|
||||
|
||||
expect(loadedList?.[0]).toBe(null);
|
||||
expect(loadedList?._refs[0]?.id).toEqual(list[0]!.id);
|
||||
|
||||
const loadedNestedList = await NestedList.load(
|
||||
list[0]!.id,
|
||||
meOnSecondPeer,
|
||||
[],
|
||||
);
|
||||
const loadedNestedList = await NestedList.load(list[0]!.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
});
|
||||
|
||||
expect(loadedList?.[0]).toBeDefined();
|
||||
expect(loadedList?.[0]?.[0]).toBe(null);
|
||||
@@ -201,11 +199,9 @@ describe("CoList resolution", async () => {
|
||||
loadedNestedList?.toJSON(),
|
||||
);
|
||||
|
||||
const loadedTwiceNestedList = await TwiceNestedList.load(
|
||||
list[0]![0]!.id,
|
||||
meOnSecondPeer,
|
||||
[],
|
||||
);
|
||||
const loadedTwiceNestedList = await TwiceNestedList.load(list[0]![0]!.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
});
|
||||
|
||||
expect(loadedList?.[0]?.[0]).toBeDefined();
|
||||
expect(loadedList?.[0]?.[0]?.[0]).toBe("a");
|
||||
@@ -255,13 +251,17 @@ describe("CoList resolution", async () => {
|
||||
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
TestList.subscribe(list.id, meOnSecondPeer, [], (subscribedList) => {
|
||||
console.log(
|
||||
"subscribedList?.[0]?.[0]?.[0]",
|
||||
subscribedList?.[0]?.[0]?.[0],
|
||||
);
|
||||
void queue.push(subscribedList);
|
||||
});
|
||||
TestList.subscribe(
|
||||
list.id,
|
||||
{ loadAs: meOnSecondPeer },
|
||||
(subscribedList) => {
|
||||
console.log(
|
||||
"subscribedList?.[0]?.[0]?.[0]",
|
||||
subscribedList?.[0]?.[0]?.[0],
|
||||
);
|
||||
void queue.push(subscribedList);
|
||||
},
|
||||
);
|
||||
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1?.[0]).toBe(null);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "../index.js";
|
||||
import { setupTwoNodes } from "./utils.js";
|
||||
|
||||
const connectedPeers = cojsonInternals.connectedPeers;
|
||||
const { connectedPeers } = cojsonInternals;
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -444,7 +444,7 @@ describe("CoMap resolution", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const loadedMap = await TestMap.load(map.id, meOnSecondPeer, {});
|
||||
const loadedMap = await TestMap.load(map.id, { loadAs: meOnSecondPeer });
|
||||
|
||||
expect(loadedMap?.color).toEqual("red");
|
||||
expect(loadedMap?.height).toEqual(10);
|
||||
@@ -452,11 +452,9 @@ describe("CoMap resolution", async () => {
|
||||
expect(loadedMap?._refs.nested?.id).toEqual(map.nested?.id);
|
||||
expect(loadedMap?._refs.nested?.value).toEqual(null);
|
||||
|
||||
const loadedNestedMap = await NestedMap.load(
|
||||
map.nested!.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
);
|
||||
const loadedNestedMap = await NestedMap.load(map.nested!.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
});
|
||||
|
||||
expect(loadedMap?.nested?.name).toEqual("nested");
|
||||
expect(loadedMap?.nested?._fancyName).toEqual("Sir nested");
|
||||
@@ -465,8 +463,7 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
const loadedTwiceNestedMap = await TwiceNestedMap.load(
|
||||
map.nested!.twiceNested!.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{ loadAs: meOnSecondPeer },
|
||||
);
|
||||
|
||||
expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sour");
|
||||
@@ -493,7 +490,7 @@ describe("CoMap resolution", async () => {
|
||||
expect(loadedMap?.nested?._refs.twiceNested?.value).toBeDefined();
|
||||
});
|
||||
|
||||
test("Subscription & auto-resolution", async () => {
|
||||
async function setupTest() {
|
||||
const { me, map } = await initNodeAndMap();
|
||||
|
||||
const [initialAsPeer, secondAsPeer] = connectedPeers("initial", "second", {
|
||||
@@ -518,28 +515,53 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
const queue = new cojsonInternals.Channel<TestMap>();
|
||||
|
||||
TestMap.subscribe(map.id, meOnSecondPeer, {}, (subscribedMap) => {
|
||||
await meOnSecondPeer.waitForAllCoValuesSync();
|
||||
|
||||
TestMap.subscribe(map.id, { loadAs: meOnSecondPeer }, (subscribedMap) => {
|
||||
// Read to property to trigger loading
|
||||
subscribedMap.nested?.twiceNested?.taste;
|
||||
void queue.push(subscribedMap);
|
||||
});
|
||||
|
||||
return { me, map, meOnSecondPeer, queue };
|
||||
}
|
||||
|
||||
test("initial subscription loads nested data progressively", async () => {
|
||||
const { queue } = await setupTest();
|
||||
|
||||
const update1 = (await queue.next()).value;
|
||||
expect(update1.nested).toEqual(null);
|
||||
|
||||
const update2 = (await queue.next()).value;
|
||||
expect(update2.nested?.name).toEqual("nested");
|
||||
});
|
||||
|
||||
test("updates to nested properties are received", async () => {
|
||||
const { map, queue } = await setupTest();
|
||||
|
||||
// Skip initial updates
|
||||
await queue.next();
|
||||
await queue.next();
|
||||
|
||||
map.nested!.name = "nestedUpdated";
|
||||
|
||||
const _ = (await queue.next()).value;
|
||||
await queue.next(); // Skip intermediate update
|
||||
const update3 = (await queue.next()).value;
|
||||
expect(update3.nested?.name).toEqual("nestedUpdated");
|
||||
|
||||
const oldTwiceNested = update3.nested!.twiceNested;
|
||||
expect(oldTwiceNested?.taste).toEqual("sour");
|
||||
});
|
||||
|
||||
test("replacing nested object triggers updates", async () => {
|
||||
const { meOnSecondPeer, queue } = await setupTest();
|
||||
|
||||
// Skip initial updates
|
||||
await queue.next();
|
||||
await queue.next();
|
||||
|
||||
const update3 = (await queue.next()).value;
|
||||
|
||||
// When assigning a new nested value, we get an update
|
||||
const newTwiceNested = TwiceNestedMap.create(
|
||||
{
|
||||
taste: "sweet",
|
||||
@@ -557,14 +579,40 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
update3.nested = newNested;
|
||||
|
||||
(await queue.next()).value;
|
||||
// const update4 = (await queue.next()).value;
|
||||
const update4b = (await queue.next()).value;
|
||||
await queue.next(); // Skip intermediate update
|
||||
const update4 = (await queue.next()).value;
|
||||
|
||||
expect(update4b.nested?.name).toEqual("newNested");
|
||||
expect(update4b.nested?.twiceNested?.taste).toEqual("sweet");
|
||||
expect(update4.nested?.name).toEqual("newNested");
|
||||
expect(update4.nested?.twiceNested?.taste).toEqual("sweet");
|
||||
});
|
||||
|
||||
test("updates to deeply nested properties are received", async () => {
|
||||
const { queue } = await setupTest();
|
||||
|
||||
// Skip to the point where we have the nested object
|
||||
await queue.next();
|
||||
await queue.next();
|
||||
const update3 = (await queue.next()).value;
|
||||
|
||||
const newTwiceNested = TwiceNestedMap.create(
|
||||
{ taste: "sweet" },
|
||||
{ owner: update3.nested!._raw.owner },
|
||||
);
|
||||
|
||||
const newNested = NestedMap.create(
|
||||
{
|
||||
name: "newNested",
|
||||
twiceNested: newTwiceNested,
|
||||
},
|
||||
{ owner: update3.nested!._raw.owner },
|
||||
);
|
||||
|
||||
update3.nested = newNested;
|
||||
|
||||
// Skip intermediate updates
|
||||
await queue.next();
|
||||
await queue.next();
|
||||
|
||||
// we get updates when the new nested value changes
|
||||
newTwiceNested.taste = "salty";
|
||||
const update5 = (await queue.next()).value;
|
||||
expect(update5.nested?.twiceNested?.taste).toEqual("salty");
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("CoPlainText", () => {
|
||||
});
|
||||
|
||||
// Load the text on the second peer
|
||||
const loaded = await CoPlainText.load(id, meOnSecondPeer);
|
||||
const loaded = await CoPlainText.load(id, { loadAs: meOnSecondPeer });
|
||||
expect(loaded).toBeDefined();
|
||||
expect(loaded!.toString()).toBe("hello world");
|
||||
});
|
||||
@@ -142,9 +142,13 @@ describe("CoPlainText", () => {
|
||||
const queue = new cojsonInternals.Channel();
|
||||
|
||||
// Subscribe to text updates
|
||||
CoPlainText.subscribe(text.id, meOnSecondPeer, (subscribedText) => {
|
||||
void queue.push(subscribedText);
|
||||
});
|
||||
CoPlainText.subscribe(
|
||||
text.id,
|
||||
{ loadAs: meOnSecondPeer },
|
||||
(subscribedText) => {
|
||||
void queue.push(subscribedText);
|
||||
},
|
||||
);
|
||||
|
||||
// Initial subscription should give us the text
|
||||
const update1 = (await queue.next()).value;
|
||||
|
||||
@@ -602,17 +602,17 @@ describe("CoRichText", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const loadedText = await CoRichText.load(text.id, meOnSecondPeer, {
|
||||
marks: [{}],
|
||||
text: [],
|
||||
const loadedText = await CoRichText.load(text.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { marks: { $each: true }, text: true },
|
||||
});
|
||||
|
||||
expect(loadedText).toBeDefined();
|
||||
expect(loadedText?.toString()).toEqual("hello world");
|
||||
|
||||
const loadedText2 = await CoRichText.load(text.id, meOnSecondPeer, {
|
||||
marks: [{}],
|
||||
text: [],
|
||||
const loadedText2 = await CoRichText.load(text.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { marks: { $each: true }, text: true },
|
||||
});
|
||||
|
||||
expect(loadedText2).toBeDefined();
|
||||
@@ -646,8 +646,10 @@ describe("CoRichText", async () => {
|
||||
|
||||
CoRichText.subscribe(
|
||||
text.id,
|
||||
meOnSecondPeer,
|
||||
{ marks: [{}], text: [] },
|
||||
{
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { marks: { $each: true }, text: true },
|
||||
},
|
||||
(subscribedText) => {
|
||||
void queue.push(subscribedText);
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isControlledAccount,
|
||||
} from "../index.js";
|
||||
import { randomSessionProvider } from "../internal.js";
|
||||
import { createJazzTestAccount, linkAccounts } from "../testing.js";
|
||||
import { waitFor } from "./utils.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
@@ -63,36 +64,41 @@ describe("Deep loading with depth arg", async () => {
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
test("loading a deeply nested object will wait until all required refs are loaded", async () => {
|
||||
const ownership = { owner: me };
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create(
|
||||
[
|
||||
InnerMap.create(
|
||||
{
|
||||
stream: TestStream.create(
|
||||
[InnermostMap.create({ value: "hello" }, ownership)],
|
||||
ownership,
|
||||
),
|
||||
},
|
||||
ownership,
|
||||
),
|
||||
],
|
||||
ownership,
|
||||
),
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
const ownership = { owner: me };
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create(
|
||||
[
|
||||
InnerMap.create(
|
||||
{
|
||||
stream: TestStream.create(
|
||||
[InnermostMap.create({ value: "hello" }, ownership)],
|
||||
ownership,
|
||||
),
|
||||
},
|
||||
ownership,
|
||||
),
|
||||
],
|
||||
ownership,
|
||||
),
|
||||
},
|
||||
ownership,
|
||||
);
|
||||
|
||||
const map1 = await TestMap.load(map.id, meOnSecondPeer, {});
|
||||
test("load without resolve", async () => {
|
||||
const map1 = await TestMap.load(map.id, { loadAs: meOnSecondPeer });
|
||||
expectTypeOf(map1).toEqualTypeOf<TestMap | undefined>();
|
||||
if (map1 === undefined) {
|
||||
throw new Error("map1 is undefined");
|
||||
}
|
||||
expect(map1.list).toBe(null);
|
||||
});
|
||||
|
||||
const map2 = await TestMap.load(map.id, meOnSecondPeer, { list: [] });
|
||||
test("load with resolve { list: true }", async () => {
|
||||
const map2 = await TestMap.load(map.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { list: true },
|
||||
});
|
||||
expectTypeOf(map2).toEqualTypeOf<
|
||||
| (TestMap & {
|
||||
list: TestList;
|
||||
@@ -102,10 +108,15 @@ describe("Deep loading with depth arg", async () => {
|
||||
if (map2 === undefined) {
|
||||
throw new Error("map2 is undefined");
|
||||
}
|
||||
expect(map2.list).not.toBe(null);
|
||||
expect(map2.list).toBeTruthy();
|
||||
expect(map2.list[0]).toBe(null);
|
||||
});
|
||||
|
||||
const map3 = await TestMap.load(map.id, meOnSecondPeer, { list: [{}] });
|
||||
test("load with resolve { list: { $each: true } }", async () => {
|
||||
const map3 = await TestMap.load(map.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { list: { $each: true } },
|
||||
});
|
||||
expectTypeOf(map3).toEqualTypeOf<
|
||||
| (TestMap & {
|
||||
list: TestList & InnerMap[];
|
||||
@@ -115,11 +126,14 @@ describe("Deep loading with depth arg", async () => {
|
||||
if (map3 === undefined) {
|
||||
throw new Error("map3 is undefined");
|
||||
}
|
||||
expect(map3.list[0]).not.toBe(null);
|
||||
expect(map3.list[0]).toBeTruthy();
|
||||
expect(map3.list[0]?.stream).toBe(null);
|
||||
});
|
||||
|
||||
const map3a = await TestMap.load(map.id, meOnSecondPeer, {
|
||||
optionalRef: {},
|
||||
test("load with resolve { optionalRef: true }", async () => {
|
||||
const map3a = await TestMap.load(map.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { optionalRef: true } as const,
|
||||
});
|
||||
expectTypeOf(map3a).toEqualTypeOf<
|
||||
| (TestMap & {
|
||||
@@ -127,9 +141,13 @@ describe("Deep loading with depth arg", async () => {
|
||||
})
|
||||
| undefined
|
||||
>();
|
||||
expect(map3a).toBeTruthy();
|
||||
});
|
||||
|
||||
const map4 = await TestMap.load(map.id, meOnSecondPeer, {
|
||||
list: [{ stream: [] }],
|
||||
test("load with resolve { list: { $each: { stream: true } } }", async () => {
|
||||
const map4 = await TestMap.load(map.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { list: { $each: { stream: true } } },
|
||||
});
|
||||
expectTypeOf(map4).toEqualTypeOf<
|
||||
| (TestMap & {
|
||||
@@ -140,12 +158,15 @@ describe("Deep loading with depth arg", async () => {
|
||||
if (map4 === undefined) {
|
||||
throw new Error("map4 is undefined");
|
||||
}
|
||||
expect(map4.list[0]?.stream).not.toBe(null);
|
||||
expect(map4.list[0]?.stream?.[me.id]).not.toBe(null);
|
||||
expect(map4.list[0]?.stream).toBeTruthy();
|
||||
expect(map4.list[0]?.stream?.[me.id]).toBeTruthy();
|
||||
expect(map4.list[0]?.stream?.byMe?.value).toBe(null);
|
||||
});
|
||||
|
||||
const map5 = await TestMap.load(map.id, meOnSecondPeer, {
|
||||
list: [{ stream: [{}] }],
|
||||
test("load with resolve { list: { $each: { stream: { $each: true } } } }", async () => {
|
||||
const map5 = await TestMap.load(map.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: { list: { $each: { stream: { $each: true } } } },
|
||||
});
|
||||
type ExpectedMap5 =
|
||||
| (TestMap & {
|
||||
@@ -165,13 +186,13 @@ describe("Deep loading with depth arg", async () => {
|
||||
})[];
|
||||
})
|
||||
| undefined;
|
||||
|
||||
expectTypeOf(map5).toEqualTypeOf<ExpectedMap5>();
|
||||
if (map5 === undefined) {
|
||||
throw new Error("map5 is undefined");
|
||||
}
|
||||
expect(map5.list[0]?.stream?.[me.id]?.value).not.toBe(null);
|
||||
expect(map5.list[0]?.stream?.byMe?.value).not.toBe(null);
|
||||
|
||||
expect(map5.list[0]?.stream?.[me.id]?.value).toBeTruthy();
|
||||
expect(map5.list[0]?.stream?.byMe?.value).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,8 +221,10 @@ class CustomAccount extends Account {
|
||||
}
|
||||
|
||||
const thisLoaded = await this.ensureLoaded({
|
||||
profile: { stream: [] },
|
||||
root: { list: [] },
|
||||
resolve: {
|
||||
profile: { stream: true },
|
||||
root: { list: true },
|
||||
},
|
||||
});
|
||||
expectTypeOf(thisLoaded).toEqualTypeOf<
|
||||
CustomAccount & {
|
||||
@@ -223,8 +246,10 @@ test("Deep loading within account", async () => {
|
||||
});
|
||||
|
||||
const meLoaded = await me.ensureLoaded({
|
||||
profile: { stream: [] },
|
||||
root: { list: [] },
|
||||
resolve: {
|
||||
profile: { stream: true },
|
||||
root: { list: true },
|
||||
},
|
||||
});
|
||||
expectTypeOf(meLoaded).toEqualTypeOf<
|
||||
CustomAccount & {
|
||||
@@ -237,8 +262,8 @@ test("Deep loading within account", async () => {
|
||||
}
|
||||
>();
|
||||
|
||||
expect(meLoaded.profile.stream).not.toBe(null);
|
||||
expect(meLoaded.root.list).not.toBe(null);
|
||||
expect(meLoaded.profile.stream).toBeTruthy();
|
||||
expect(meLoaded.root.list).toBeTruthy();
|
||||
});
|
||||
|
||||
class RecordLike extends CoMap.Record(co.ref(TestMap)) {}
|
||||
@@ -284,9 +309,12 @@ test("Deep loading a record-like coMap", async () => {
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const recordLoaded = await RecordLike.load(record.id, meOnSecondPeer, [
|
||||
{ list: [{}] },
|
||||
]);
|
||||
const recordLoaded = await RecordLike.load(record.id, {
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
$each: { list: { $each: true } },
|
||||
},
|
||||
});
|
||||
expectTypeOf(recordLoaded).toEqualTypeOf<
|
||||
| (RecordLike & {
|
||||
[key: string]: TestMap & {
|
||||
@@ -299,9 +327,200 @@ test("Deep loading a record-like coMap", async () => {
|
||||
throw new Error("recordLoaded is undefined");
|
||||
}
|
||||
expect(recordLoaded.key1?.list).not.toBe(null);
|
||||
expect(recordLoaded.key1?.list).not.toBe(undefined);
|
||||
expect(recordLoaded.key1?.list).toBeTruthy();
|
||||
expect(recordLoaded.key2?.list).not.toBe(null);
|
||||
expect(recordLoaded.key2?.list).not.toBe(undefined);
|
||||
expect(recordLoaded.key2?.list).toBeTruthy();
|
||||
});
|
||||
|
||||
test("The resolve type doesn't accept extra keys", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const me = await CustomAccount.create({
|
||||
creationProps: { name: "Hermes Puggington" },
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
try {
|
||||
const meLoaded = await me.ensureLoaded({
|
||||
resolve: {
|
||||
// @ts-expect-error
|
||||
profile: { stream: true, extraKey: true },
|
||||
// @ts-expect-error
|
||||
root: { list: true, extraKey: true },
|
||||
},
|
||||
});
|
||||
|
||||
await me.ensureLoaded({
|
||||
resolve: {
|
||||
// @ts-expect-error
|
||||
root: { list: { $each: true, extraKey: true } },
|
||||
},
|
||||
});
|
||||
|
||||
await me.ensureLoaded({
|
||||
resolve: {
|
||||
root: { list: true },
|
||||
// @ts-expect-error
|
||||
extraKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
expectTypeOf(meLoaded).toEqualTypeOf<
|
||||
CustomAccount & {
|
||||
profile: CustomProfile & {
|
||||
stream: TestStream;
|
||||
extraKey: never;
|
||||
};
|
||||
root: TestMap & {
|
||||
list: TestList;
|
||||
extraKey: never;
|
||||
};
|
||||
}
|
||||
>();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Deep loading with unauthorized account", async () => {
|
||||
const bob = await createJazzTestAccount({
|
||||
creationProps: { name: "Bob" },
|
||||
});
|
||||
|
||||
const alice = await createJazzTestAccount({
|
||||
creationProps: { name: "Alice" },
|
||||
});
|
||||
|
||||
linkAccounts(bob, alice);
|
||||
|
||||
await alice.waitForAllCoValuesSync();
|
||||
|
||||
const onlyBob = bob;
|
||||
const group = Group.create(bob);
|
||||
|
||||
group.addMember(alice, "reader");
|
||||
|
||||
test("unaccessible root", async () => {
|
||||
const map = TestMap.create({ list: TestList.create([], group) }, onlyBob);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, { loadAs: alice });
|
||||
|
||||
expect(mapOnAlice).toBe(undefined);
|
||||
});
|
||||
|
||||
test("unaccessible list", async () => {
|
||||
const map = TestMap.create({ list: TestList.create([], onlyBob) }, group);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, { loadAs: alice });
|
||||
expect(mapOnAlice).toBeTruthy();
|
||||
|
||||
const mapWithListOnAlice = await TestMap.load(map.id, {
|
||||
resolve: { list: true },
|
||||
loadAs: alice,
|
||||
});
|
||||
|
||||
expect(mapWithListOnAlice).toBe(undefined);
|
||||
});
|
||||
|
||||
test("unaccessible list element", async () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create(
|
||||
[
|
||||
InnerMap.create(
|
||||
{
|
||||
stream: TestStream.create([], group),
|
||||
},
|
||||
onlyBob,
|
||||
),
|
||||
],
|
||||
group,
|
||||
),
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, {
|
||||
resolve: { list: { $each: true } },
|
||||
loadAs: alice,
|
||||
});
|
||||
|
||||
expect(mapOnAlice).toBe(undefined);
|
||||
});
|
||||
|
||||
test("unaccessible optional element", async () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create([], group),
|
||||
optionalRef: InnermostMap.create({ value: "hello" }, onlyBob),
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, {
|
||||
loadAs: alice,
|
||||
resolve: { optionalRef: true } as const,
|
||||
});
|
||||
expect(mapOnAlice).toBe(undefined);
|
||||
|
||||
expect(mapOnAlice?.optionalRef).toBe(undefined);
|
||||
expect(mapOnAlice?.optionalRef?.value).toBe(undefined);
|
||||
});
|
||||
|
||||
test("unaccessible stream", async () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create(
|
||||
[
|
||||
InnerMap.create(
|
||||
{
|
||||
stream: TestStream.create([], onlyBob),
|
||||
},
|
||||
group,
|
||||
),
|
||||
],
|
||||
group,
|
||||
),
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, {
|
||||
resolve: { list: { $each: { stream: true } } },
|
||||
loadAs: alice,
|
||||
});
|
||||
|
||||
expect(mapOnAlice).toBe(undefined);
|
||||
});
|
||||
|
||||
test("unaccessible stream element", async () => {
|
||||
const map = TestMap.create(
|
||||
{
|
||||
list: TestList.create(
|
||||
[
|
||||
InnerMap.create(
|
||||
{
|
||||
stream: TestStream.create(
|
||||
[InnermostMap.create({ value: "hello" }, onlyBob)],
|
||||
group,
|
||||
),
|
||||
},
|
||||
group,
|
||||
),
|
||||
],
|
||||
group,
|
||||
),
|
||||
},
|
||||
group,
|
||||
);
|
||||
|
||||
const mapOnAlice = await TestMap.load(map.id, {
|
||||
resolve: { list: { $each: { stream: { $each: true } } } },
|
||||
loadAs: alice,
|
||||
});
|
||||
|
||||
expect(mapOnAlice).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test("doesn't break on Map.Record key deletion when the key is referenced in the depth", async () => {
|
||||
@@ -317,7 +536,10 @@ test("doesn't break on Map.Record key deletion when the key is referenced in the
|
||||
});
|
||||
|
||||
const spy = vi.fn();
|
||||
const unsub = snapStore.subscribe({ profile1: {}, profile2: {} }, spy);
|
||||
const unsub = snapStore.subscribe(
|
||||
{ resolve: { profile1: true, profile2: true } },
|
||||
spy,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
|
||||
@@ -330,7 +552,9 @@ test("doesn't break on Map.Record key deletion when the key is referenced in the
|
||||
|
||||
await expect(
|
||||
snapStore.ensureLoaded({
|
||||
profile1: {},
|
||||
resolve: {
|
||||
profile1: true,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("Failed to deeply load CoValue " + snapStore.id);
|
||||
});
|
||||
@@ -357,7 +581,7 @@ test("throw when calling ensureLoaded on a ref that's required but missing", asy
|
||||
|
||||
await expect(
|
||||
root.ensureLoaded({
|
||||
profile: {},
|
||||
resolve: { profile: true },
|
||||
}),
|
||||
).rejects.toThrow("Failed to deeply load CoValue " + root.id);
|
||||
});
|
||||
@@ -374,7 +598,7 @@ test("throw when calling ensureLoaded on a ref that is not defined in the schema
|
||||
|
||||
await expect(
|
||||
root.ensureLoaded({
|
||||
profile: {},
|
||||
resolve: { profile: true },
|
||||
}),
|
||||
).rejects.toThrow("Failed to deeply load CoValue " + root.id);
|
||||
});
|
||||
@@ -399,7 +623,7 @@ test("should not throw when calling ensureLoaded a record with a deleted ref", a
|
||||
);
|
||||
|
||||
let value: any;
|
||||
let unsub = root.subscribe([{}], (v) => {
|
||||
let unsub = root.subscribe({ resolve: { $each: true } }, (v) => {
|
||||
value = v;
|
||||
});
|
||||
|
||||
@@ -412,7 +636,7 @@ test("should not throw when calling ensureLoaded a record with a deleted ref", a
|
||||
unsub();
|
||||
|
||||
value = undefined;
|
||||
unsub = root.subscribe([{}], (v) => {
|
||||
unsub = root.subscribe({ resolve: { $each: true } }, (v) => {
|
||||
value = v;
|
||||
});
|
||||
|
||||
|
||||
@@ -106,18 +106,16 @@ describe("Group inheritance", () => {
|
||||
|
||||
const mapInChild = TestMap.create({ title: "In Child" }, { owner: group });
|
||||
|
||||
const mapAsReader = await TestMap.load(mapInChild.id, reader, {});
|
||||
const mapAsReader = await TestMap.load(mapInChild.id, { loadAs: reader });
|
||||
expect(mapAsReader?.title).toBe("In Child");
|
||||
|
||||
await parentGroup.removeMember(reader);
|
||||
|
||||
mapInChild.title = "In Child (updated)";
|
||||
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(
|
||||
mapInChild.id,
|
||||
reader,
|
||||
{},
|
||||
);
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(mapInChild.id, {
|
||||
loadAs: reader,
|
||||
});
|
||||
expect(mapAsReaderAfterUpdate?.title).toBe("In Child");
|
||||
});
|
||||
|
||||
@@ -145,18 +143,18 @@ describe("Group inheritance", () => {
|
||||
{ owner: group },
|
||||
);
|
||||
|
||||
const mapAsReader = await TestMap.load(mapInGrandChild.id, reader, {});
|
||||
const mapAsReader = await TestMap.load(mapInGrandChild.id, {
|
||||
loadAs: reader,
|
||||
});
|
||||
expect(mapAsReader?.title).toBe("In Grand Child");
|
||||
|
||||
await grandParentGroup.removeMember(reader);
|
||||
|
||||
mapInGrandChild.title = "In Grand Child (updated)";
|
||||
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(
|
||||
mapInGrandChild.id,
|
||||
reader,
|
||||
{},
|
||||
);
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(mapInGrandChild.id, {
|
||||
loadAs: reader,
|
||||
});
|
||||
expect(mapAsReaderAfterUpdate?.title).toBe("In Grand Child");
|
||||
});
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ describe("Inbox", () => {
|
||||
);
|
||||
const resultId = await inboxSender.sendMessage(message);
|
||||
|
||||
const result = await Message.load(resultId, receiver, {});
|
||||
const result = await Message.load(resultId, { loadAs: receiver });
|
||||
expect(result?.text).toBe("Responded from the inbox");
|
||||
|
||||
unsubscribe();
|
||||
|
||||
@@ -88,23 +88,16 @@ describe("SchemaUnion", () => {
|
||||
{ owner: me },
|
||||
);
|
||||
|
||||
const loadedButtonWidget = await loadCoValue(
|
||||
WidgetUnion,
|
||||
buttonWidget.id,
|
||||
me,
|
||||
{},
|
||||
);
|
||||
const loadedSliderWidget = await loadCoValue(
|
||||
WidgetUnion,
|
||||
sliderWidget.id,
|
||||
me,
|
||||
{},
|
||||
);
|
||||
const loadedButtonWidget = await loadCoValue(WidgetUnion, buttonWidget.id, {
|
||||
loadAs: me,
|
||||
});
|
||||
const loadedSliderWidget = await loadCoValue(WidgetUnion, sliderWidget.id, {
|
||||
loadAs: me,
|
||||
});
|
||||
const loadedCheckboxWidget = await loadCoValue(
|
||||
WidgetUnion,
|
||||
checkboxWidget.id,
|
||||
me,
|
||||
{},
|
||||
{ loadAs: me },
|
||||
);
|
||||
|
||||
expect(loadedButtonWidget).toBeInstanceOf(RedButtonWidget);
|
||||
@@ -121,8 +114,7 @@ describe("SchemaUnion", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
WidgetUnion,
|
||||
buttonWidget.id,
|
||||
me,
|
||||
{},
|
||||
{ loadAs: me, syncResolution: true },
|
||||
(value: BaseWidget) => {
|
||||
if (value instanceof BlueButtonWidget) {
|
||||
expect(value.label).toBe(currentValue);
|
||||
@@ -130,8 +122,6 @@ describe("SchemaUnion", () => {
|
||||
throw new Error("Unexpected widget type");
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
true,
|
||||
);
|
||||
currentValue = "Changed";
|
||||
buttonWidget.label = "Changed";
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
cojsonInternals,
|
||||
} from "../index.js";
|
||||
import {
|
||||
type DepthsIn,
|
||||
ID,
|
||||
Resolved,
|
||||
createCoValueObservable,
|
||||
subscribeToCoValue,
|
||||
} from "../internal.js";
|
||||
@@ -65,8 +65,7 @@ describe("subscribeToCoValue", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{ loadAs: meOnSecondPeer },
|
||||
updateFn,
|
||||
);
|
||||
|
||||
@@ -126,9 +125,11 @@ describe("subscribeToCoValue", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{
|
||||
messages: [],
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
messages: true,
|
||||
},
|
||||
},
|
||||
updateFn,
|
||||
);
|
||||
@@ -156,16 +157,20 @@ describe("subscribeToCoValue", () => {
|
||||
const chatRoom = createChatRoom(me, "General");
|
||||
const updateFn = vi.fn();
|
||||
|
||||
const { messages } = await chatRoom.ensureLoaded({ messages: [{}] });
|
||||
const { messages } = await chatRoom.ensureLoaded({
|
||||
resolve: { messages: { $each: true } },
|
||||
});
|
||||
|
||||
messages.push(createMessage(me, "Hello"));
|
||||
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{
|
||||
messages: [{}],
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
messages: { $each: true },
|
||||
},
|
||||
},
|
||||
updateFn,
|
||||
);
|
||||
@@ -204,9 +209,13 @@ describe("subscribeToCoValue", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{
|
||||
messages: [{}],
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
messages: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
updateFn,
|
||||
);
|
||||
@@ -249,13 +258,15 @@ describe("subscribeToCoValue", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
reactions: [],
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
messages: {
|
||||
$each: {
|
||||
reactions: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
updateFn,
|
||||
);
|
||||
@@ -315,13 +326,15 @@ describe("subscribeToCoValue", () => {
|
||||
const unsubscribe = subscribeToCoValue(
|
||||
ChatRoom,
|
||||
chatRoom.id,
|
||||
meOnSecondPeer,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
reactions: [],
|
||||
loadAs: meOnSecondPeer,
|
||||
resolve: {
|
||||
messages: {
|
||||
$each: {
|
||||
reactions: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
updateFn,
|
||||
);
|
||||
@@ -372,14 +385,15 @@ describe("createCoValueObservable", () => {
|
||||
it("should update currentValue when subscribed", async () => {
|
||||
const { me, meOnSecondPeer } = await setupAccount();
|
||||
const testMap = createTestMap(me);
|
||||
const observable = createCoValueObservable<TestMap, DepthsIn<TestMap>>();
|
||||
const observable = createCoValueObservable();
|
||||
const mockListener = vi.fn();
|
||||
|
||||
const unsubscribe = observable.subscribe(
|
||||
TestMap,
|
||||
testMap.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{
|
||||
loadAs: meOnSecondPeer,
|
||||
},
|
||||
() => {
|
||||
mockListener();
|
||||
},
|
||||
@@ -400,14 +414,15 @@ describe("createCoValueObservable", () => {
|
||||
it("should reset to undefined after unsubscribe", async () => {
|
||||
const { me, meOnSecondPeer } = await setupAccount();
|
||||
const testMap = createTestMap(me);
|
||||
const observable = createCoValueObservable<TestMap, DepthsIn<TestMap>>();
|
||||
const observable = createCoValueObservable();
|
||||
const mockListener = vi.fn();
|
||||
|
||||
const unsubscribe = observable.subscribe(
|
||||
TestMap,
|
||||
testMap.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{
|
||||
loadAs: meOnSecondPeer,
|
||||
},
|
||||
() => {
|
||||
mockListener();
|
||||
},
|
||||
@@ -422,13 +437,15 @@ describe("createCoValueObservable", () => {
|
||||
|
||||
it("should return null if the coValue is not found", async () => {
|
||||
const { meOnSecondPeer } = await setupAccount();
|
||||
const observable = createCoValueObservable<TestMap, DepthsIn<TestMap>>();
|
||||
const observable = createCoValueObservable<
|
||||
TestMap,
|
||||
Resolved<TestMap, {}>
|
||||
>();
|
||||
|
||||
const unsubscribe = observable.subscribe(
|
||||
TestMap,
|
||||
"co_z123" as ID<TestMap>,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{ loadAs: meOnSecondPeer },
|
||||
() => {},
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("Jazz Test Sync", () => {
|
||||
map._raw.set("test", "value");
|
||||
|
||||
// Verify account2 can see the group
|
||||
const loadedMap = await CoMap.load(map.id, account2, {});
|
||||
const loadedMap = await CoMap.load(map.id, { loadAs: account2 });
|
||||
expect(loadedMap).toBeDefined();
|
||||
expect(loadedMap?._raw.get("test")).toBe("value");
|
||||
});
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
AuthSecretStorage,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
JazzAuthContext,
|
||||
JazzContextType,
|
||||
JazzGuestContext,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
subscribeToCoValue,
|
||||
} from "jazz-tools";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
@@ -59,16 +60,16 @@ export function createUseAccountComposables<Acc extends Account>() {
|
||||
me: ComputedRef<Acc>;
|
||||
logOut: () => void;
|
||||
};
|
||||
function useAccount<D extends DepthsIn<Acc>>(
|
||||
depth: D,
|
||||
): {
|
||||
me: ComputedRef<DeeplyLoaded<Acc, D> | undefined | null>;
|
||||
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): {
|
||||
me: ComputedRef<Resolved<Acc, R> | undefined | null>;
|
||||
logOut: () => void;
|
||||
};
|
||||
function useAccount<D extends DepthsIn<Acc>>(
|
||||
depth?: D,
|
||||
): {
|
||||
me: ComputedRef<Acc | DeeplyLoaded<Acc, D> | undefined | null>;
|
||||
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): {
|
||||
me: ComputedRef<Acc | Resolved<Acc, R> | undefined | null>;
|
||||
logOut: () => void;
|
||||
} {
|
||||
const context = useJazzContext();
|
||||
@@ -85,16 +86,16 @@ export function createUseAccountComposables<Acc extends Account>() {
|
||||
|
||||
const contextMe = context.value.me as Acc;
|
||||
|
||||
const me = useCoState<Acc, D>(
|
||||
const me = useCoState<Acc, R>(
|
||||
contextMe.constructor as CoValueClass<Acc>,
|
||||
contextMe.id,
|
||||
depth,
|
||||
options,
|
||||
);
|
||||
|
||||
return {
|
||||
me: computed(() => {
|
||||
const value =
|
||||
depth === undefined
|
||||
options?.resolve === undefined
|
||||
? me.value || toRaw((context.value as JazzAuthContext<Acc>).me)
|
||||
: me.value;
|
||||
|
||||
@@ -107,18 +108,16 @@ export function createUseAccountComposables<Acc extends Account>() {
|
||||
function useAccountOrGuest(): {
|
||||
me: ComputedRef<Acc | AnonymousJazzAgent>;
|
||||
};
|
||||
function useAccountOrGuest<D extends DepthsIn<Acc>>(
|
||||
depth: D,
|
||||
): {
|
||||
me: ComputedRef<
|
||||
DeeplyLoaded<Acc, D> | undefined | null | AnonymousJazzAgent
|
||||
>;
|
||||
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): {
|
||||
me: ComputedRef<Resolved<Acc, R> | undefined | null | AnonymousJazzAgent>;
|
||||
};
|
||||
function useAccountOrGuest<D extends DepthsIn<Acc>>(
|
||||
depth?: D,
|
||||
): {
|
||||
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): {
|
||||
me: ComputedRef<
|
||||
Acc | DeeplyLoaded<Acc, D> | undefined | null | AnonymousJazzAgent
|
||||
Acc | Resolved<Acc, R> | undefined | null | AnonymousJazzAgent
|
||||
>;
|
||||
} {
|
||||
const context = useJazzContext();
|
||||
@@ -131,16 +130,16 @@ export function createUseAccountComposables<Acc extends Account>() {
|
||||
"me" in context.value ? (context.value.me as Acc) : undefined,
|
||||
);
|
||||
|
||||
const me = useCoState<Acc, D>(
|
||||
const me = useCoState<Acc, R>(
|
||||
contextMe.value?.constructor as CoValueClass<Acc>,
|
||||
contextMe.value?.id,
|
||||
depth,
|
||||
options,
|
||||
);
|
||||
|
||||
if ("me" in context.value) {
|
||||
return {
|
||||
me: computed(() =>
|
||||
depth === undefined
|
||||
options?.resolve === undefined
|
||||
? me.value || toRaw((context.value as JazzAuthContext<Acc>).me)
|
||||
: me.value,
|
||||
),
|
||||
@@ -163,12 +162,12 @@ const { useAccount, useAccountOrGuest } =
|
||||
|
||||
export { useAccount, useAccountOrGuest };
|
||||
|
||||
export function useCoState<V extends CoValue, D>(
|
||||
export function useCoState<V extends CoValue, const R extends RefsToResolve<V>>(
|
||||
Schema: CoValueClass<V>,
|
||||
id: MaybeRef<ID<CoValue> | undefined>,
|
||||
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
|
||||
): Ref<DeeplyLoaded<V, D> | undefined | null> {
|
||||
const state: ShallowRef<DeeplyLoaded<V, D> | undefined | null> =
|
||||
options?: { resolve?: RefsToResolveStrict<V, R> },
|
||||
): Ref<Resolved<V, R> | undefined | null> {
|
||||
const state: ShallowRef<Resolved<V, R> | undefined | null> =
|
||||
shallowRef(undefined);
|
||||
const context = useJazzContext();
|
||||
|
||||
@@ -179,7 +178,7 @@ export function useCoState<V extends CoValue, D>(
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
watch(
|
||||
[() => unref(id), () => context, () => Schema, () => depth],
|
||||
[() => unref(id), () => context, () => Schema, () => options],
|
||||
() => {
|
||||
if (unsubscribe) unsubscribe();
|
||||
|
||||
@@ -189,17 +188,23 @@ export function useCoState<V extends CoValue, D>(
|
||||
unsubscribe = subscribeToCoValue(
|
||||
Schema,
|
||||
idValue,
|
||||
"me" in context.value
|
||||
? toRaw(context.value.me)
|
||||
: toRaw(context.value.guest),
|
||||
depth,
|
||||
{
|
||||
resolve: options?.resolve,
|
||||
loadAs:
|
||||
"me" in context.value
|
||||
? toRaw(context.value.me)
|
||||
: toRaw(context.value.guest),
|
||||
onUnavailable: () => {
|
||||
state.value = null;
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
state.value = null;
|
||||
},
|
||||
syncResolution: true,
|
||||
},
|
||||
(value) => {
|
||||
state.value = value;
|
||||
},
|
||||
() => {
|
||||
state.value = null;
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
|
||||
@@ -46,7 +46,9 @@ describe("useAcceptInvite", () => {
|
||||
expect(acceptedId).toBeDefined();
|
||||
});
|
||||
|
||||
const accepted = await TestMap.load(acceptedId!, account, {});
|
||||
const accepted = await TestMap.load(acceptedId!, {
|
||||
loadAs: account,
|
||||
});
|
||||
|
||||
expect(accepted?.value).toEqual("hello");
|
||||
});
|
||||
|
||||
@@ -39,7 +39,9 @@ describe("useAccount", () => {
|
||||
const [result] = withJazzTestSetup(
|
||||
() =>
|
||||
useAccount({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
|
||||
@@ -51,7 +51,9 @@ describe("useAccountOrGuest", () => {
|
||||
const [result] = withJazzTestSetup(
|
||||
() =>
|
||||
useAccountOrGuest({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
@@ -68,7 +70,9 @@ describe("useAccountOrGuest", () => {
|
||||
const [result] = withJazzTestSetup(
|
||||
() =>
|
||||
useAccountOrGuest({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
|
||||
@@ -91,7 +91,9 @@ describe("useCoState", () => {
|
||||
const [result] = withJazzTestSetup(
|
||||
() =>
|
||||
useCoState(TestMap, map.id, {
|
||||
nested: {},
|
||||
resolve: {
|
||||
nested: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
@@ -165,7 +167,9 @@ describe("useCoState", () => {
|
||||
});
|
||||
|
||||
const [result] = withJazzTestSetup(() =>
|
||||
useCoState(TestMap, map.id as ID<CoValue>, []),
|
||||
useCoState(TestMap, map.id as ID<CoValue>, {
|
||||
resolve: true,
|
||||
}),
|
||||
);
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
Ref<TestMap | null | undefined, TestMap | null | undefined>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Form } from "./Form.tsx";
|
||||
import { Logo } from "./Logo.tsx";
|
||||
|
||||
function App() {
|
||||
const { me } = useAccount({ profile: {}, root: {} });
|
||||
const { me } = useAccount({ resolve: { profile: true, root: true } });
|
||||
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
export function Form() {
|
||||
const { me } = useAccount({ profile: {}, root: {} });
|
||||
const { me } = useAccount({ resolve: { profile: true, root: true } });
|
||||
|
||||
if (!me) return null;
|
||||
|
||||
|
||||
@@ -2,34 +2,42 @@ import {
|
||||
Account,
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DepthsIn,
|
||||
ID,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
subscribeToCoValue,
|
||||
} from "jazz-tools";
|
||||
|
||||
export function waitForCoValue<T extends CoValue>(
|
||||
export function waitForCoValue<
|
||||
T extends CoValue,
|
||||
const R extends RefsToResolve<T>,
|
||||
>(
|
||||
coMap: CoValueClass<T>,
|
||||
valueId: ID<T>,
|
||||
account: Account,
|
||||
predicate: (value: T) => boolean,
|
||||
depth: DepthsIn<T>,
|
||||
options: { loadAs: Account; resolve?: RefsToResolveStrict<T, R> },
|
||||
) {
|
||||
return new Promise<T>((resolve) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
function subscribe() {
|
||||
subscribeToCoValue(
|
||||
coMap,
|
||||
valueId,
|
||||
account,
|
||||
depth,
|
||||
{
|
||||
loadAs: options.loadAs,
|
||||
resolve: options.resolve,
|
||||
onUnavailable: () => {
|
||||
setTimeout(subscribe, 100);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
reject(new Error("Unauthorized"));
|
||||
},
|
||||
},
|
||||
(value, unsubscribe) => {
|
||||
if (predicate(value)) {
|
||||
resolve(value);
|
||||
unsubscribe();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setTimeout(subscribe, 100);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ function getIdParam() {
|
||||
|
||||
export function ConcurrentChanges() {
|
||||
const [id, setId] = useState(getIdParam);
|
||||
const counter = useCoState(Counter, id, []);
|
||||
const counter = useCoState(Counter, id);
|
||||
const { me } = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,9 @@ export function DownloaderPeer(props: { testCoMapId: ID<UploadedFile> }) {
|
||||
|
||||
useEffect(() => {
|
||||
async function run(me: Account, uploadedFileId: ID<UploadedFile>) {
|
||||
const uploadedFile = await UploadedFile.load(uploadedFileId, me, {});
|
||||
const uploadedFile = await UploadedFile.load(uploadedFileId, {
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
if (!uploadedFile) {
|
||||
throw new Error("Uploaded file not found");
|
||||
@@ -21,7 +23,9 @@ export function DownloaderPeer(props: { testCoMapId: ID<UploadedFile> }) {
|
||||
|
||||
uploadedFile.coMapDownloaded = true;
|
||||
|
||||
await FileStream.loadAsBlob(uploadedFile._refs.file.id, me);
|
||||
await FileStream.loadAsBlob(uploadedFile._refs.file.id, {
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
uploadedFile.syncCompleted = true;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,8 @@ export function UploaderPeer() {
|
||||
await waitForCoValue(
|
||||
UploadedFile,
|
||||
file.id,
|
||||
account.me,
|
||||
(value) => value.syncCompleted,
|
||||
{},
|
||||
{ loadAs: account.me },
|
||||
);
|
||||
|
||||
iframe.remove();
|
||||
|
||||
@@ -53,11 +53,11 @@ export function InboxPage() {
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!id) return;
|
||||
const account = await Account.load(id, me, {});
|
||||
const account = await Account.load(id);
|
||||
|
||||
if (!account) return;
|
||||
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
group.addMember(account, "writer");
|
||||
const pingPong = PingPong.create({ ping: Date.now() }, { owner: group });
|
||||
|
||||
|
||||
@@ -95,14 +95,13 @@ function SharedCoMapWithChildren(props: {
|
||||
level: number;
|
||||
revealLevels: number;
|
||||
}) {
|
||||
const coMap = useCoState(SharedCoMap, props.id, {});
|
||||
const { me } = useAccount();
|
||||
const coMap = useCoState(SharedCoMap, props.id);
|
||||
const nextLevel = props.level + 1;
|
||||
|
||||
const addChild = () => {
|
||||
if (!me || !coMap) return;
|
||||
if (!coMap) return;
|
||||
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
|
||||
const child = SharedCoMap.create(
|
||||
{ value: "CoValue child " + nextLevel },
|
||||
@@ -118,7 +117,7 @@ function SharedCoMapWithChildren(props: {
|
||||
|
||||
while (node?._refs.child?.id) {
|
||||
const parentGroup = node._owner as Group;
|
||||
node = await SharedCoMap.load(node._refs.child.id, me, {});
|
||||
node = await SharedCoMap.load(node._refs.child.id);
|
||||
|
||||
if (node) {
|
||||
const childGroup = node._owner as Group;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createInviteLink } from "jazz-react";
|
||||
import { useAcceptInvite, useAccount, useCoState } from "jazz-react";
|
||||
import { CoList, CoMap, Group, ID, co } from "jazz-tools";
|
||||
import { useAcceptInvite, useCoState } from "jazz-react";
|
||||
import { Account, CoList, CoMap, Group, ID, co } from "jazz-tools";
|
||||
import { useState } from "react";
|
||||
|
||||
class SharedCoMap extends CoMap {
|
||||
@@ -10,15 +10,14 @@ class SharedCoMap extends CoMap {
|
||||
class SharedCoList extends CoList.Of(co.ref(SharedCoMap)) {}
|
||||
|
||||
export function WriteOnlyRole() {
|
||||
const { me } = useAccount();
|
||||
const [id, setId] = useState<ID<SharedCoList> | undefined>(undefined);
|
||||
const [inviteLinks, setInviteLinks] = useState<Record<string, string>>({});
|
||||
const coList = useCoState(SharedCoList, id, []);
|
||||
const coList = useCoState(SharedCoList, id);
|
||||
|
||||
const createCoList = async () => {
|
||||
if (!me || id) return;
|
||||
if (id) return;
|
||||
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
|
||||
const coList = SharedCoList.create([], { owner: group });
|
||||
|
||||
@@ -35,7 +34,7 @@ export function WriteOnlyRole() {
|
||||
};
|
||||
|
||||
const addNewItem = async () => {
|
||||
if (!me || !coList) return;
|
||||
if (!coList) return;
|
||||
|
||||
const group = coList._owner as Group;
|
||||
const coMap = SharedCoMap.create({ value: "" }, { owner: group });
|
||||
@@ -52,7 +51,7 @@ export function WriteOnlyRole() {
|
||||
if (
|
||||
member.account &&
|
||||
member.role !== "admin" &&
|
||||
member.account.id !== me.id
|
||||
member.account.id !== Account.getMe().id
|
||||
) {
|
||||
coListGroup.removeMember(member.account);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user