Compare commits
22 Commits
cojson@0.1
...
fix/option
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cb11e38dd | ||
|
|
f3e4bacb33 | ||
|
|
626d43f07b | ||
|
|
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -152,9 +156,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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -163,7 +169,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;
|
||||
@@ -173,17 +181,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();
|
||||
|
||||
@@ -14,8 +14,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,9 +29,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();
|
||||
|
||||
@@ -23,7 +23,9 @@ function Member({
|
||||
accountId,
|
||||
role,
|
||||
}: { accountId: ID<Account>; role?: string }) {
|
||||
const account = useCoState(Account, accountId, { profile: {} });
|
||||
const account = useCoState(Account, accountId, {
|
||||
resolve: { profile: true },
|
||||
});
|
||||
|
||||
if (!account?.profile) return;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,15 @@ export class RawGroup<
|
||||
roleOfInternal(
|
||||
accountID: RawAccountID | AgentID | typeof EVERYONE,
|
||||
): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
|
||||
const roleHere = this.get(accountID);
|
||||
let roleHere = this.get(accountID);
|
||||
|
||||
if (!roleHere) {
|
||||
const everyoneRole = this.get(EVERYONE);
|
||||
|
||||
if (everyoneRole && everyoneRole !== "revoked") {
|
||||
roleHere = everyoneRole;
|
||||
}
|
||||
}
|
||||
|
||||
if (roleHere === "revoked") {
|
||||
return undefined;
|
||||
|
||||
@@ -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,
|
||||
@@ -842,3 +843,98 @@ describe("extend with role mapping", () => {
|
||||
expect(mapOnNode2.get("test")).toEqual("Written from the admin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("roleOf", () => {
|
||||
test("returns direct role assignments", () => {
|
||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||
const group = node.createGroup();
|
||||
const account = new LocalNode(
|
||||
...randomAnonymousAccountAndSessionID(),
|
||||
Crypto,
|
||||
).account;
|
||||
|
||||
group.addMember(account, "writer");
|
||||
expect(group.roleOf(account.id as RawAccountID)).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;
|
||||
|
||||
expect(group.roleOf(account.id as RawAccountID)).toEqual(undefined);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
currentAccount.profile.name = username;
|
||||
|
||||
@@ -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 { JazzContext, JazzContextManagerContext } from "./provider.js";
|
||||
@@ -76,12 +77,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 {
|
||||
@@ -92,26 +92,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 () => {};
|
||||
@@ -121,12 +120,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],
|
||||
@@ -143,12 +150,12 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
me: Acc;
|
||||
logOut: () => void;
|
||||
};
|
||||
function useAccount<D extends DepthsIn<Acc>>(
|
||||
depth: D,
|
||||
): { me: DeeplyLoaded<Acc, D> | undefined | null; logOut: () => void };
|
||||
function useAccount<D extends DepthsIn<Acc>>(
|
||||
depth?: D,
|
||||
): { me: Acc | DeeplyLoaded<Acc, D> | undefined | null; logOut: () => void } {
|
||||
function useAccount<const R extends RefsToResolve<Acc> = true>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): { me: Resolved<Acc, R> | undefined | null; logOut: () => void };
|
||||
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): { me: Acc | Resolved<Acc, R> | undefined | null; logOut: () => void } {
|
||||
const context = useJazzContext<Acc>();
|
||||
const contextManager = useJazzContextManager<Acc>();
|
||||
|
||||
@@ -158,11 +165,9 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
);
|
||||
}
|
||||
|
||||
const observable = useCoValueObservable<Acc, D>();
|
||||
const observable = useCoValueObservable<Acc, R>();
|
||||
|
||||
const me = React.useSyncExternalStore<
|
||||
DeeplyLoaded<Acc, D> | undefined | null
|
||||
>(
|
||||
const me = React.useSyncExternalStore<Resolved<Acc, R> | undefined | null>(
|
||||
React.useCallback(
|
||||
(callback) => {
|
||||
return subscribeToContextManager(contextManager, () => {
|
||||
@@ -178,16 +183,18 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
|
||||
const Schema = agent.constructor as CoValueClass<Acc>;
|
||||
|
||||
return observable
|
||||
.getCurrentObservable()
|
||||
.subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
agent,
|
||||
depth ?? ([] as D),
|
||||
callback,
|
||||
callback,
|
||||
);
|
||||
return observable.getCurrentObservable().subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
{
|
||||
loadAs: agent,
|
||||
resolve: options?.resolve,
|
||||
onUnauthorized: callback,
|
||||
onUnavailable: callback,
|
||||
syncResolution: true,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
});
|
||||
},
|
||||
[contextManager],
|
||||
@@ -197,7 +204,7 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
);
|
||||
|
||||
return {
|
||||
me: depth === undefined ? me || context.me : me,
|
||||
me: options?.resolve === undefined ? me || context.me : me,
|
||||
logOut: context.logOut,
|
||||
};
|
||||
}
|
||||
@@ -205,22 +212,20 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
function useAccountOrGuest(): {
|
||||
me: Acc | AnonymousJazzAgent;
|
||||
};
|
||||
function useAccountOrGuest<D extends DepthsIn<Acc>>(
|
||||
depth: D,
|
||||
): { me: DeeplyLoaded<Acc, D> | undefined | null | AnonymousJazzAgent };
|
||||
function useAccountOrGuest<D extends DepthsIn<Acc>>(
|
||||
depth?: D,
|
||||
): {
|
||||
me: Acc | DeeplyLoaded<Acc, D> | undefined | null | AnonymousJazzAgent;
|
||||
} {
|
||||
function useAccountOrGuest<
|
||||
const R extends RefsToResolve<Acc> = true,
|
||||
>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): { me: Resolved<Acc, R> | undefined | null | AnonymousJazzAgent };
|
||||
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
|
||||
resolve?: RefsToResolveStrict<Acc, R>;
|
||||
}): { me: Acc | Resolved<Acc, R> | undefined | null | AnonymousJazzAgent } {
|
||||
const context = useJazzContext<Acc>();
|
||||
const contextManager = useJazzContextManager<Acc>();
|
||||
|
||||
const observable = useCoValueObservable<Acc, D>();
|
||||
const observable = useCoValueObservable<Acc, R>();
|
||||
|
||||
const me = React.useSyncExternalStore<
|
||||
DeeplyLoaded<Acc, D> | undefined | null
|
||||
>(
|
||||
const me = React.useSyncExternalStore<Resolved<Acc, R> | undefined | null>(
|
||||
React.useCallback(
|
||||
(callback) => {
|
||||
return subscribeToContextManager(contextManager, () => {
|
||||
@@ -234,16 +239,18 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
|
||||
const Schema = agent.constructor as CoValueClass<Acc>;
|
||||
|
||||
return observable
|
||||
.getCurrentObservable()
|
||||
.subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
agent,
|
||||
depth ?? ([] as D),
|
||||
callback,
|
||||
callback,
|
||||
);
|
||||
return observable.getCurrentObservable().subscribe(
|
||||
Schema,
|
||||
agent.id,
|
||||
{
|
||||
loadAs: agent,
|
||||
resolve: options?.resolve,
|
||||
onUnauthorized: callback,
|
||||
onUnavailable: callback,
|
||||
syncResolution: true,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
});
|
||||
},
|
||||
[contextManager],
|
||||
@@ -254,7 +261,7 @@ export function createUseAccountHooks<Acc extends Account>() {
|
||||
|
||||
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 };
|
||||
|
||||
@@ -46,7 +46,9 @@ describe("useAccount", () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAccount({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
|
||||
@@ -49,7 +49,9 @@ describe("useAccountOrGuest", () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAccountOrGuest({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: true,
|
||||
},
|
||||
}),
|
||||
{
|
||||
account,
|
||||
@@ -66,7 +68,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 };
|
||||
|
||||
@@ -65,21 +66,12 @@ export interface Register {}
|
||||
|
||||
export type RegisteredAccount = Register extends { Account: infer Acc } ? Acc : Account;
|
||||
|
||||
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');
|
||||
@@ -91,7 +83,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;
|
||||
@@ -103,10 +95,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 {
|
||||
@@ -120,24 +112,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) {
|
||||
@@ -146,17 +126,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;
|
||||
}
|
||||
@@ -172,24 +152,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(() => {
|
||||
@@ -199,20 +172,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;
|
||||
|
||||
@@ -18,19 +18,23 @@ import {
|
||||
type CoValue,
|
||||
CoValueBase,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
MembersSym,
|
||||
Ref,
|
||||
type RefEncoded,
|
||||
RefIfCoValue,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
type Schema,
|
||||
SchemaInit,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
ensureCoValueLoaded,
|
||||
inspect,
|
||||
loadCoValue,
|
||||
loadCoValueWithoutMe,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
subscriptionsScopes,
|
||||
@@ -182,7 +186,9 @@ export class Account extends CoValueBase implements CoValue {
|
||||
inviteSecret,
|
||||
);
|
||||
|
||||
return loadCoValue(coValueClass, valueID, this as Account, []);
|
||||
return loadCoValue(coValueClass, valueID, {
|
||||
loadAs: this,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
@@ -288,73 +294,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;
|
||||
|
||||
@@ -8,11 +8,14 @@ import type {
|
||||
import type {
|
||||
CoValue,
|
||||
CoValueClass,
|
||||
DeeplyLoaded,
|
||||
DepthsIn,
|
||||
ID,
|
||||
RefEncoded,
|
||||
RefsToResolve,
|
||||
RefsToResolveStrict,
|
||||
Resolved,
|
||||
Schema,
|
||||
SubscribeListenerOptions,
|
||||
SubscribeRestArgs,
|
||||
} from "../internal.js";
|
||||
import {
|
||||
CoValueBase,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
ensureCoValueLoaded,
|
||||
loadCoValueWithoutMe,
|
||||
parseGroupCreateOptions,
|
||||
parseSubscribeRestArgs,
|
||||
subscribeToCoValueWithoutMe,
|
||||
subscribeToExistingCoValue,
|
||||
} from "../internal.js";
|
||||
@@ -188,73 +192,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>,
|
||||
) {
|
||||
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;
|
||||
|
||||
@@ -234,7 +234,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);
|
||||
});
|
||||
@@ -253,7 +255,7 @@ describe("ContextManager", () => {
|
||||
value: 1,
|
||||
});
|
||||
} else {
|
||||
const { root } = await this.ensureLoaded({ root: {} });
|
||||
const { root } = await this.ensureLoaded({ resolve: { root: true } });
|
||||
|
||||
root.value = 2;
|
||||
}
|
||||
@@ -276,7 +278,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);
|
||||
});
|
||||
@@ -303,10 +307,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;
|
||||
|
||||
@@ -334,7 +344,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");
|
||||
});
|
||||
|
||||
@@ -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, waitFor } from "./utils.js";
|
||||
|
||||
const connectedPeers = cojsonInternals.connectedPeers;
|
||||
const { connectedPeers } = cojsonInternals;
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -445,7 +445,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);
|
||||
@@ -453,11 +453,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");
|
||||
@@ -466,8 +464,7 @@ describe("CoMap resolution", async () => {
|
||||
|
||||
const loadedTwiceNestedMap = await TwiceNestedMap.load(
|
||||
map.nested!.twiceNested!.id,
|
||||
meOnSecondPeer,
|
||||
{},
|
||||
{ loadAs: meOnSecondPeer },
|
||||
);
|
||||
|
||||
expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sour");
|
||||
@@ -494,7 +491,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", {
|
||||
@@ -519,28 +516,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",
|
||||
@@ -558,14 +580,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);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CoFeed,
|
||||
CoList,
|
||||
CoMap,
|
||||
Group,
|
||||
ID,
|
||||
Profile,
|
||||
SessionID,
|
||||
@@ -14,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();
|
||||
@@ -62,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;
|
||||
@@ -101,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[];
|
||||
@@ -114,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 & {
|
||||
@@ -126,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 & {
|
||||
@@ -139,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 & {
|
||||
@@ -164,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,8 +223,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 & {
|
||||
@@ -224,8 +248,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 & {
|
||||
@@ -238,8 +264,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)) {}
|
||||
@@ -285,9 +311,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 & {
|
||||
@@ -300,9 +329,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 () => {
|
||||
@@ -326,7 +546,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());
|
||||
|
||||
@@ -339,7 +562,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);
|
||||
});
|
||||
@@ -366,7 +591,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);
|
||||
});
|
||||
@@ -383,7 +608,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);
|
||||
});
|
||||
@@ -408,7 +633,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;
|
||||
});
|
||||
|
||||
@@ -421,7 +646,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;
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("Custom accounts and groups", async () => {
|
||||
expect(group.nMembers).toBe(2);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
group.subscribe({}, (update) => {
|
||||
group.subscribe((update) => {
|
||||
const meAsMember = update.members.find((member) => {
|
||||
return member.id === me.id && member.account?.profile;
|
||||
});
|
||||
@@ -119,18 +119,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");
|
||||
});
|
||||
|
||||
@@ -158,18 +156,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");
|
||||
});
|
||||
|
||||
|
||||
@@ -122,7 +122,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,
|
||||
|
||||
@@ -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