Compare commits

...

22 Commits

Author SHA1 Message Date
Guido D'Orsi
9cb11e38dd feat(deepLoading): return undefined when an optional field is not accessible 2025-02-26 12:56:06 +01:00
Guido D'Orsi
f3e4bacb33 Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-25 17:46:22 +01:00
Guido D'Orsi
626d43f07b Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-21 18:27:14 +01:00
Guido D'Orsi
706ca62feb Merge pull request #1363 from garden-co/unauthorized-deep-loading
fix: check CoValue permissions when loading/subscribing
2025-02-13 09:55:16 +01:00
Guido D'Orsi
01523dcca3 fix: check CoValue permissions when loading/subscribing 2025-02-13 09:19:19 +01:00
Guido D'Orsi
77f039b561 Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-13 09:18:47 +01:00
Guido D'Orsi
d661ba77be chore: improve pre-release comment output 2025-02-12 11:32:05 +01:00
Guido D'Orsi
f8fbc59b6f Merge remote-tracking branch 'origin/main' into jazz-581-rfc-new-deep-loading-api 2025-02-11 16:26:12 +01:00
Guido D'Orsi
cce0d22007 fix: update resolve property 2025-02-11 15:15:44 +01:00
Guido D'Orsi
e3ff76e9cb Merge remote-tracking branch 'origin/authv2' into jazz-581-rfc-new-deep-loading-api 2025-02-11 15:12:51 +01:00
Guido D'Orsi
4cbf71bff7 Merge pull request #1338 from garden-co/new-deep-loading-extra-props-fix
fix: disallow extra props in the resolve type
2025-02-11 14:38:43 +01:00
Guido D'Orsi
ceb060243a fix: disallow extra props in the resolve type 2025-02-10 20:04:32 +01:00
Anselm
a70bebb96a Clean up new deep-loading API 2025-01-29 14:39:16 +00:00
Anselm
b3b2507c35 Merge branch 'main' into jazz-581-rfc-new-deep-loading-api 2025-01-27 11:36:08 +00:00
Anselm
6a8fa16b49 Fix form and chat-rn-clerk examples 2024-12-16 15:56:48 +00:00
Anselm
1f08807701 Merge branch 'main' into jazz-581-rfc-new-deep-loading-api 2024-12-16 15:37:39 +00:00
Anselm
ba4a7f6170 Remove temp vite config 2024-12-16 15:35:06 +00:00
Anselm
a2854e3602 Upgrade to minor version change 2024-12-16 11:25:00 +00:00
Anselm
4ea87dc494 Add changeset 2024-12-16 10:55:51 +00:00
Anselm
d8c87c5314 Use $each instead of each 2024-12-16 10:54:25 +00:00
Anselm
46f624a12e Replace 'items' with 'each' 2024-12-13 16:28:40 +00:00
Anselm
86ce770f38 Implement clearer syntax for deep loading 2024-12-13 15:12:42 +00:00
89 changed files with 1922 additions and 1103 deletions

View File

@@ -0,0 +1,6 @@
---
"jazz-tools": minor
"cojson": minor
---
Check CoValue access permissions when loading

View File

@@ -0,0 +1,5 @@
---
"jazz-tools": minor
---
Implement new API for deep loading

View File

@@ -0,0 +1,5 @@
---
"cojson": minor
---
Return the EVERYONE role if the account is not direct a member of the group

View File

@@ -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);

View File

@@ -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, {});

View File

@@ -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(() => {

View File

@@ -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)

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -4,7 +4,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
export function Orders() {
const { me } = useAccount({
root: { orders: [] },
resolve: { root: { orders: true } },
});
return (

View File

@@ -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;

View File

@@ -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,
},
},
},
});

View File

@@ -9,7 +9,7 @@ import { getNextTrack, getPrevTrack } from "./lib/getters";
export function useMediaPlayer() {
const { me } = useAccount({
root: {},
resolve: { root: true },
});
const playState = usePlayState();

View File

@@ -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,
},
},
});

View File

@@ -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,
},
},
},
},
});

View File

@@ -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 ?? [];

View File

@@ -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;

View File

@@ -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>) {

View File

@@ -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;

View File

@@ -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,
},
},
},
});

View File

@@ -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;

View File

@@ -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(

View File

@@ -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;

View File

@@ -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 (

View File

@@ -13,7 +13,7 @@ export function OrganizationPage() {
.organizationId;
const organization = useCoState(Organization, paramOrganizationId, {
projects: [],
resolve: { projects: true },
});
if (!organization) return <p>Loading organization...</p>;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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 } },
}),
]);

View File

@@ -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;

View File

@@ -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("");

View File

@@ -122,7 +122,7 @@ export default function App() {
function HomeScreen() {
const { me } = useAccount({
root: { projects: [{}] },
resolve: { root: { projects: { $each: true } } },
});
const navigate = useNavigate();

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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");
});
});

View File

@@ -107,7 +107,9 @@ export class JazzClerkAuth {
});
const currentAccount = await Account.getMe().ensureLoaded({
profile: {},
resolve: {
profile: true,
},
});
const username = getClerkUsername(clerkClient);

View File

@@ -81,7 +81,9 @@ describe("JazzClerkAuth", () => {
});
const me = await Account.getMe().ensureLoaded({
profile: {},
resolve: {
profile: true,
},
});
expect(me.profile.name).toBe("Guido");
});

View File

@@ -83,7 +83,9 @@ export class BrowserPasskeyAuth {
});
const currentAccount = await Account.getMe().ensureLoaded({
profile: {},
resolve: {
profile: true,
},
});
currentAccount.profile.name = username;

View File

@@ -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");

View File

@@ -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 };

View File

@@ -46,7 +46,9 @@ describe("useAccount", () => {
const { result } = renderHook(
() =>
useAccount({
root: {},
resolve: {
root: true,
},
}),
{
account,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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");
});

View File

@@ -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");
});

View File

@@ -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
);
});

View File

@@ -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");
});

View File

@@ -66,7 +66,9 @@ export class DemoAuth {
}
const currentAccount = await Account.getMe().ensureLoaded({
profile: {},
resolve: {
profile: true,
},
});
currentAccount.profile.name = username;

View File

@@ -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);
}
/**

View File

@@ -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);
}
/**

View File

@@ -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 */

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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) {

View File

@@ -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,
);
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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");
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");

View File

@@ -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;

View File

@@ -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);
},

View File

@@ -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;
});

View File

@@ -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");
});

View File

@@ -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();

View File

@@ -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";

View File

@@ -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 },
() => {},
);

View File

@@ -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");
});

View File

@@ -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 },

View File

@@ -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");
});

View File

@@ -39,7 +39,9 @@ describe("useAccount", () => {
const [result] = withJazzTestSetup(
() =>
useAccount({
root: {},
resolve: {
root: true,
},
}),
{
account,

View File

@@ -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,

View File

@@ -91,7 +91,9 @@ describe("useCoState", () => {
const [result] = withJazzTestSetup(
() =>
useCoState(TestMap, map.id, {
nested: {},
resolve: {
nested: true,
},
}),
{
account,

View File

@@ -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();

View File

@@ -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;

View File

@@ -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);
},
);
}

View File

@@ -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(() => {

View File

@@ -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;
}

View File

@@ -46,9 +46,8 @@ export function UploaderPeer() {
await waitForCoValue(
UploadedFile,
file.id,
account.me,
(value) => value.syncCompleted,
{},
{ loadAs: account.me },
);
iframe.remove();

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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);
}