Compare commits

...

11 Commits

Author SHA1 Message Date
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
79 changed files with 987 additions and 915 deletions

View File

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

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("");
useEffect(() => {

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 (

File diff suppressed because one or more lines are too long

View File

@@ -19,10 +19,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();
@@ -46,8 +43,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

@@ -24,11 +24,13 @@ import {
export async function uploadMusicTracks(files: Iterable<File>) {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
resolve: {
root: {
rootPlaylist: {
tracks: true,
},
playlists: true,
},
playlists: [],
},
});
@@ -64,8 +66,10 @@ export async function uploadMusicTracks(files: Iterable<File>) {
export async function createNewPlaylist() {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
playlists: [],
resolve: {
root: {
playlists: true,
},
},
});
@@ -153,9 +157,11 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
export async function updateActivePlaylist(playlist?: Playlist) {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
activePlaylist: {},
rootPlaylist: {},
resolve: {
root: {
activePlaylist: true,
rootPlaylist: true,
},
},
});
@@ -166,7 +172,7 @@ export async function updateActivePlaylist(playlist?: Playlist) {
export async function updateActiveTrack(track: MusicTrack) {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {},
resolve: { root: true },
});
if (!me) return;

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

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

@@ -5,9 +5,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,
},
},
},
});
@@ -23,9 +25,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

@@ -10,7 +10,7 @@ export function useUploadExampleData() {
async function uploadOnboardingData() {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {},
resolve: { root: true },
});
if (!me) throw new Error("Me not resolved");

View File

@@ -19,8 +19,10 @@ function ImportEmployee({
const { employeeCoId } = useParams();
const navigate = useNavigate();
const employees = useCoState(EmployeeCoList, employeeListCoId, [{}]);
const employee = useCoState(CoEmployee, employeeCoId as ID<CoEmployee>, {});
const employees = useCoState(EmployeeCoList, employeeListCoId, {
resolve: { $each: true },
});
const employee = useCoState(CoEmployee, employeeCoId as ID<CoEmployee>);
useEffect(() => {
if (!employee || !employees) return;

View File

@@ -10,7 +10,9 @@ export function EmployeeList({
}: {
employeeListCoId: ID<EmployeeCoList>;
}) {
const employees = useCoState(EmployeeCoList, employeeListCoId, [{}]);
const employees = useCoState(EmployeeCoList, employeeListCoId, {
resolve: { $each: true },
});
if (!employees) {
return <div>Loading...</div>;

View File

@@ -22,7 +22,9 @@ export function NewEmployee({
const navigate = useNavigate();
const { me } = useAccount();
const employees = useCoState(EmployeeCoList, employeeListCoId, [{}]);
const employees = useCoState(EmployeeCoList, employeeListCoId, {
resolve: { $each: true },
});
const [employeeName, setEmployeeName] = useState<string>("");

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

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

@@ -4,10 +4,7 @@
"type": "module",
"main": "./dist/app.js",
"types": "./dist/app.d.ts",
"files": [
"dist/**",
"src"
],
"files": ["dist/**", "src"],
"scripts": {
"dev": "vite build --watch",
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist",

View File

@@ -64,7 +64,7 @@ describe("startWorker integration", () => {
await map.waitForSync();
const mapOnWorker2 = await TestMap.load(map.id, worker2.worker, {});
const mapOnWorker2 = await TestMap.load(map.id, worker2.worker);
expect(mapOnWorker2?.value).toBe("test");
@@ -89,7 +89,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, worker2.worker);
expect(mapOnWorker2?.value).toBe("test");

View File

@@ -5,12 +5,13 @@ import {
AnonymousJazzAgent,
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
InboxSender,
RefsToResolve,
Resolved,
createCoValueObservable,
} from "jazz-tools";
import { RefsToResolveStrict } from "jazz-tools";
import { JazzContext, JazzContextType } from "./provider.js";
export function useJazzContext<Acc extends Account>() {
@@ -25,28 +26,39 @@ export function useJazzContext<Acc extends Account>() {
return value;
}
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<V> | undefined,
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
): DeeplyLoaded<V, D> | undefined {
options?: { resolve?: RefsToResolveStrict<V, R> },
): Resolved<V, R> | undefined {
const context = useJazzContext();
const [observable] = React.useState(() =>
createCoValueObservable({
createCoValueObservable<V, R>({
syncResolution: true,
}),
);
const value = React.useSyncExternalStore<DeeplyLoaded<V, D> | undefined>(
const value = React.useSyncExternalStore<Resolved<V, R> | undefined>(
React.useCallback(
(callback) => {
if (!id) return () => {};
const agent = "me" in context ? context.me : context.guest;
return observable.subscribe(Schema, id, agent, depth, callback);
return observable.subscribe(
Schema,
id,
{
loadAs: agent,
resolve: options?.resolve,
},
callback,
);
},
[Schema, id, context],
),
@@ -62,12 +74,12 @@ export function createUseAccountHooks<Acc extends Account>() {
me: Acc;
logOut: () => void;
};
function useAccount<D extends DepthsIn<Acc>>(
depth: D,
): { me: DeeplyLoaded<Acc, D> | undefined; logOut: () => void };
function useAccount<D extends DepthsIn<Acc>>(
depth?: D,
): { me: Acc | DeeplyLoaded<Acc, D> | undefined; logOut: () => void } {
function useAccount<const R extends RefsToResolve<Acc> = true>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): { me: Resolved<Acc, R> | undefined; logOut: () => void };
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): { me: Acc | Resolved<Acc, R> | undefined; logOut: () => void } {
const context = useJazzContext<Acc>();
if (!("me" in context)) {
@@ -76,10 +88,14 @@ export function createUseAccountHooks<Acc extends Account>() {
);
}
const me = useCoState<Acc, D>(context.AccountSchema, context.me.id, depth);
const me = useCoState<Acc, R>(
context.AccountSchema,
context.me.id,
options,
);
return {
me: depth === undefined ? me || context.me : me,
me: options?.resolve === undefined ? me || context.me : me,
logOut: context.logOut,
};
}
@@ -87,21 +103,27 @@ export function createUseAccountHooks<Acc extends Account>() {
function useAccountOrGuest(): {
me: Acc | AnonymousJazzAgent;
};
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth: D,
): { me: DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent };
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth?: D,
): { me: Acc | DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent } {
function useAccountOrGuest<
const R extends RefsToResolve<Acc> = true,
>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): { me: Resolved<Acc, R> | undefined | AnonymousJazzAgent };
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): { me: Acc | Resolved<Acc, R> | undefined | AnonymousJazzAgent } {
const context = useJazzContext<Acc>();
const contextMe = "me" in context ? context.me : undefined;
const me = useCoState<Acc, D>(context.AccountSchema, contextMe?.id, depth);
const me = useCoState<Acc, R>(
context.AccountSchema,
contextMe?.id,
options,
);
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

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

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

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

@@ -7,13 +7,14 @@ import type {
AnonymousJazzAgent,
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID
ID,
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 };
@@ -44,18 +45,12 @@ 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; 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; logOut: () => void } {
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
): { me: Resolved<RegisteredAccount, R> | undefined; logOut: () => void };
export function useAccount<const R extends RefsToResolve<RegisteredAccount>>(
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined; logOut: () => void } {
const ctx = getJazzContext<RegisteredAccount>();
if (!ctx?.current) {
throw new Error('useAccount must be used within a JazzProvider');
@@ -67,7 +62,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 BrowserContext<RegisteredAccount>).me;
@@ -79,10 +74,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 BrowserContext<RegisteredAccount>).me.id,
depth
options
);
return {
@@ -96,17 +91,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 | 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 | AnonymousJazzAgent } {
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
): { me: Resolved<RegisteredAccount, R> | undefined | AnonymousJazzAgent };
export function useAccountOrGuest<R extends RefsToResolve<RegisteredAccount>>(
options?: { resolve?: RefsToResolveStrict<RegisteredAccount, R> }
): { me: RegisteredAccount | Resolved<RegisteredAccount, R> | undefined | AnonymousJazzAgent } {
const ctx = getJazzContext<RegisteredAccount>();
if (!ctx?.current) {
@@ -115,17 +105,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 BrowserContext<RegisteredAccount>)?.me
: me.current;
}
@@ -141,24 +131,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<V> | undefined,
depth: D = [] as D
options?: { resolve?: RefsToResolveStrict<V, R> }
): {
current?: DeeplyLoaded<V, D>;
current?: Resolved<V, R>;
} {
const ctx = getJazzContext<RegisteredAccount>();
// Create state and a stable observable
let state = $state.raw<DeeplyLoaded<V, D> | undefined>(undefined);
let state = $state.raw<Resolved<V, R> | undefined>(undefined);
// Effect to handle subscription
$effect(() => {
@@ -168,12 +151,13 @@ 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 },
(value) => {
// Get current value from our stable observable
state = value;
@@ -219,6 +203,7 @@ export function useAcceptInvite<V extends CoValue>({
// Subscribe to the onAccept function.
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
_onAccept;
// Subscribe to the onAccept function.
untrack(() => {

View File

@@ -18,19 +18,22 @@ import {
type CoValue,
CoValueBase,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
MembersSym,
Ref,
type RefEncoded,
RefIfCoValue,
RefsToResolve,
RefsToResolveStrict,
Resolved,
type Schema,
SchemaInit,
SubscribeRestArgs,
ensureCoValueLoaded,
inspect,
loadCoValue,
loadCoValueWithoutMe,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
subscriptionsScopes,
@@ -166,7 +169,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
}
@@ -272,73 +277,65 @@ 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: {
resolve?: RefsToResolveStrict<A, R>;
loadAs?: Account | AnonymousJazzAgent;
},
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> | undefined> {
return ensureCoValueLoaded(this, depth);
options: { resolve: RefsToResolveStrict<A, R> },
): Promise<Resolved<A, R> | undefined> {
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,18 @@ 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,
SubscribeRestArgs,
UnCo,
} from "../internal.js";
import {
@@ -31,11 +32,10 @@ import {
co,
ensureCoValueLoaded,
inspect,
isAccountInstance,
isRefEncoded,
loadCoValueWithoutMe,
parseCoValueCreateOptions,
subscribeToCoValue,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
} from "../internal.js";
@@ -326,59 +326,42 @@ 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: {
resolve?: RefsToResolveStrict<F, R>;
loadAs?: Account | AnonymousJazzAgent;
},
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);
}
/**
@@ -388,11 +371,11 @@ export class CoFeed<Item = any> extends CoValueBase implements CoValue {
* or undefined if it cannot be loaded that deeply
* @category Subscription & Loading
*/
ensureLoaded<S extends CoFeed, Depth>(
this: S,
depth: Depth & DepthsIn<S>,
): Promise<DeeplyLoaded<S, Depth> | undefined> {
return ensureCoValueLoaded(this, depth);
ensureLoaded<F extends CoFeed, const R extends RefsToResolve<F>>(
this: F,
options?: { resolve?: RefsToResolveStrict<F, R> },
): Promise<Resolved<F, R> | undefined> {
return ensureCoValueLoaded(this, options);
}
/**
@@ -401,12 +384,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);
}
/**
@@ -761,34 +753,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
@@ -796,12 +764,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);
}
},
);
});
}
@@ -900,78 +873,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,14 @@ import type {
CoValue,
CoValueClass,
CoValueFromRaw,
DeeplyLoaded,
DepthsIn,
ID,
RefEncoded,
RefsToResolve,
RefsToResolveStrict,
Resolved,
Schema,
SchemaFor,
SubscribeRestArgs,
UnCo,
} from "../internal.js";
import {
@@ -24,6 +26,7 @@ import {
loadCoValueWithoutMe,
makeRefs,
parseCoValueCreateOptions,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
subscriptionsScopes,
@@ -360,24 +363,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 +402,27 @@ 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: {
resolve?: RefsToResolveStrict<L, R>;
loadAs?: Account | AnonymousJazzAgent;
},
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 +432,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> | undefined> {
return ensureCoValueLoaded(this, depth);
options: { resolve: RefsToResolveStrict<L, R> },
): Promise<Resolved<L, R> | undefined> {
return ensureCoValueLoaded(this, options);
}
/**
@@ -462,12 +448,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,15 @@ import type {
AnonymousJazzAgent,
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
IfCo,
RefEncoded,
RefIfCoValue,
RefsToResolve,
RefsToResolveStrict,
Resolved,
Schema,
SubscribeRestArgs,
co,
} from "../internal.js";
import {
@@ -32,6 +34,7 @@ import {
loadCoValueWithoutMe,
makeRefs,
parseCoValueCreateOptions,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
subscriptionsScopes,
@@ -432,24 +435,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 +473,27 @@ 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: {
resolve?: RefsToResolveStrict<M, R>;
loadAs?: Account | AnonymousJazzAgent;
},
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 +525,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> | undefined> {
return ensureCoValueLoaded(this, depth);
options: { resolve: RefsToResolveStrict<M, R> },
): Promise<Resolved<M, R> | undefined> {
return ensureCoValueLoaded(this, options);
}
/**
@@ -555,12 +541,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,19 @@ 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,
SubscribeRestArgs,
} from "../internal.js";
import {
inspect,
isAccountInstance,
loadCoValue,
subscribeToCoValue,
loadCoValueWithoutMe,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
} from "../internal.js";
import { Account } from "./account.js";
@@ -122,9 +128,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 +163,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: { loadAs?: Account | AnonymousJazzAgent },
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 +191,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

@@ -8,19 +8,22 @@ import { type CoValue, type ID } from "./interfaces.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function fulfillsDepth(depth: any, value: CoValue): boolean {
if (depth === true || depth === undefined) {
return true;
}
if (
value._type === "CoMap" ||
value._type === "Group" ||
value._type === "Account"
) {
if (Array.isArray(depth) && depth.length === 1) {
if ("$each" in depth) {
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)
? item && fulfillsDepth(depth.$each, item)
: ((value as CoMap)._schema[ItemsSym] as RefEncoded<CoValue>)!
.optional;
});
@@ -44,29 +47,19 @@ export function fulfillsDepth(depth: any, value: CoValue): boolean {
return true;
}
} 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,
);
}
const itemDepth = depth.$each;
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,
);
} 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,
);
}
const itemDepth = depth.$each;
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,
);
} else if (
value._type === "BinaryCoStream" ||
value._type === "CoPlainText"
@@ -79,58 +72,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,
@@ -139,41 +149,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<
@@ -185,32 +196,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,13 @@ import type {
import type {
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
RefEncoded,
RefsToResolve,
RefsToResolveStrict,
Resolved,
Schema,
SubscribeRestArgs,
} from "../internal.js";
import {
CoValueBase,
@@ -21,6 +23,7 @@ import {
ensureCoValueLoaded,
loadCoValueWithoutMe,
parseGroupCreateOptions,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
subscribeToExistingCoValue,
} from "../internal.js";
@@ -187,73 +190,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: { resolve?: RefsToResolveStrict<G, R>; loadAs?: Account },
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> | undefined> {
return ensureCoValueLoaded(this, depth);
options?: { resolve?: RefsToResolveStrict<G, R> },
): Promise<Resolved<G, R> | undefined> {
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,36 +159,39 @@ 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<V>,
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<V>,
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,
options,
(value, unsubscribe) => {
resolve(value);
unsubscribe();
@@ -196,63 +203,97 @@ export function loadCoValue<V extends CoValue, Depth>(
});
}
export function ensureCoValueLoaded<V extends CoValue, Depth>(
export function ensureCoValueLoaded<
V extends CoValue,
const R extends RefsToResolve<V>,
>(
existing: V,
depth: Depth & DepthsIn<V>,
): Promise<DeeplyLoaded<V, Depth> | undefined> {
return loadCoValue(
existing.constructor as CoValueClass<V>,
existing.id,
existing._loadedAs,
depth,
);
options?: { resolve?: RefsToResolveStrict<V, R> } | undefined,
): Promise<Resolved<V, R> | undefined> {
return loadCoValue(existing.constructor as CoValueClass<V>, existing.id, {
loadAs: existing._loadedAs,
resolve: options?.resolve,
});
}
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 SubscribeRestArgs<V extends CoValue, R extends RefsToResolve<V>> =
| [
options: {
resolve?: RefsToResolveStrict<V, R>;
loadAs?: Account | AnonymousJazzAgent;
},
listener: SubscribeListener<V, R>,
]
| [listener: SubscribeListener<V, R>];
export function parseSubscribeRestArgs<
V extends CoValue,
R extends RefsToResolve<V>,
>(
args: SubscribeRestArgs<V, R>,
): {
options: { resolve?: RefsToResolveStrict<V, R> };
listener: SubscribeListener<V, R>;
} {
if (args.length === 2) {
if (
typeof args[0] === "object" &&
"resolve" in args[0] &&
typeof args[1] === "function"
) {
return { options: { resolve: args[0].resolve }, 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<V>,
asOrDepth: Account | AnonymousJazzAgent | (Depth & DepthsIn<V>),
depthOrListener:
| (Depth & DepthsIn<V>)
| ((value: DeeplyLoaded<V, Depth>) => void),
listener?: (value: DeeplyLoaded<V, Depth>) => void,
options: {
resolve?: RefsToResolveStrict<V, R>;
loadAs?: Account | AnonymousJazzAgent;
},
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<V>,
as: Account | AnonymousJazzAgent,
depth: Depth & DepthsIn<V>,
listener: (value: DeeplyLoaded<V, Depth>, unsubscribe: () => void) => void,
options: {
resolve?: RefsToResolveStrict<V, R>;
loadAs: Account | AnonymousJazzAgent;
},
listener: SubscribeListener<V, R>,
onUnavailable?: () => void,
syncResolution?: boolean,
): () => 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;
@@ -267,11 +308,8 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
value,
cls as CoValueClass<V> & CoValueFromRaw<V>,
(update, subscription) => {
if (fulfillsDepth(depth, update)) {
listener(
update as DeeplyLoaded<V, Depth>,
subscription.unsubscribeAll,
);
if (fulfillsDepth(options.resolve, update)) {
listener(update as Resolved<V, R>, subscription.unsubscribeAll);
}
},
);
@@ -298,17 +336,22 @@ export function subscribeToCoValue<V extends CoValue, Depth>(
};
}
export function createCoValueObservable<V extends CoValue, Depth>(options?: {
export function createCoValueObservable<
V extends CoValue,
const R extends RefsToResolve<V>,
>(observableOptions?: {
syncResolution?: boolean;
}) {
let currentValue: DeeplyLoaded<V, Depth> | undefined = undefined;
let currentValue: Resolved<V, R> | undefined = undefined;
let subscriberCount = 0;
function subscribe(
cls: CoValueClass<V>,
id: ID<V>,
as: Account | AnonymousJazzAgent,
depth: Depth & DepthsIn<V>,
options: {
loadAs: Account | AnonymousJazzAgent;
resolve?: RefsToResolveStrict<V, R>;
},
listener: () => void,
onUnavailable?: () => void,
) {
@@ -317,14 +360,13 @@ export function createCoValueObservable<V extends CoValue, Depth>(options?: {
const unsubscribe = subscribeToCoValue(
cls,
id,
as,
depth,
options,
(value) => {
currentValue = value;
listener();
},
onUnavailable,
options?.syncResolution,
observableOptions?.syncResolution,
);
return () => {
@@ -344,16 +386,18 @@ 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> } | undefined,
listener: SubscribeListener<V, R>,
): () => void {
return subscribeToCoValue(
existing.constructor as CoValueClass<V>,
existing.id,
existing._loadedAs,
depth,
{ loadAs: existing._loadedAs, resolve: options?.resolve },
listener,
);
}

View File

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

@@ -121,15 +121,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);
@@ -145,8 +146,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(
@@ -208,9 +208,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);
@@ -325,7 +329,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",
@@ -358,9 +364,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);
@@ -429,9 +439,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();
@@ -447,12 +455,11 @@ 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([]);
stream.push(new Uint8Array([2]));
stream.end();

View File

@@ -180,16 +180,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);
@@ -200,11 +198,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");
@@ -253,13 +249,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

@@ -418,7 +418,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);
@@ -426,11 +426,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");
@@ -439,8 +437,7 @@ describe("CoMap resolution", async () => {
const loadedTwiceNestedMap = await TwiceNestedMap.load(
map.nested!.twiceNested!.id,
meOnSecondPeer,
{},
{ loadAs: meOnSecondPeer },
);
expect(loadedMap?.nested?.twiceNested?.taste).toEqual("sour");
@@ -491,7 +488,7 @@ describe("CoMap resolution", async () => {
const queue = new cojsonInternals.Channel<TestMap>();
TestMap.subscribe(map.id, meOnSecondPeer, {}, (subscribedMap) => {
TestMap.subscribe(map.id, { loadAs: meOnSecondPeer }, (subscribedMap) => {
// Read to property to trigger loading
subscribedMap.nested?.twiceNested?.taste;
void queue.push(subscribedMap);

View File

@@ -108,7 +108,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");
});
@@ -140,9 +140,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();
@@ -645,8 +645,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

@@ -1,5 +1,5 @@
const Crypto = await WasmCrypto.create();
import { connectedPeers } from "cojson/src/streamUtils.ts";
import { cojsonInternals } from "cojson";
import { describe, expect, expectTypeOf, test } from "vitest";
import {
Account,
@@ -17,6 +17,8 @@ import {
} from "../index.web.js";
import { randomSessionProvider } from "../internal.js";
const { connectedPeers } = cojsonInternals;
class TestMap extends CoMap {
list = co.ref(TestList);
optionalRef = co.ref(InnermostMap, { optional: true });
@@ -81,14 +83,17 @@ describe("Deep loading with depth arg", async () => {
ownership,
);
const map1 = await TestMap.load(map.id, meOnSecondPeer, {});
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: [] });
const map2 = await TestMap.load(map.id, {
loadAs: meOnSecondPeer,
resolve: { list: true },
});
expectTypeOf(map2).toEqualTypeOf<
| (TestMap & {
list: TestList;
@@ -101,7 +106,10 @@ describe("Deep loading with depth arg", async () => {
expect(map2.list).not.toBe(null);
expect(map2.list[0]).toBe(null);
const map3 = await TestMap.load(map.id, meOnSecondPeer, { list: [{}] });
const map3 = await TestMap.load(map.id, {
loadAs: meOnSecondPeer,
resolve: { list: { $each: true } },
});
expectTypeOf(map3).toEqualTypeOf<
| (TestMap & {
list: TestList & InnerMap[];
@@ -114,8 +122,9 @@ describe("Deep loading with depth arg", async () => {
expect(map3.list[0]).not.toBe(null);
expect(map3.list[0]?.stream).toBe(null);
const map3a = await TestMap.load(map.id, meOnSecondPeer, {
optionalRef: {},
const map3a = await TestMap.load(map.id, {
loadAs: meOnSecondPeer,
resolve: { optionalRef: true } as const,
});
expectTypeOf(map3a).toEqualTypeOf<
| (TestMap & {
@@ -124,8 +133,9 @@ describe("Deep loading with depth arg", async () => {
| undefined
>();
const map4 = await TestMap.load(map.id, meOnSecondPeer, {
list: [{ stream: [] }],
const map4 = await TestMap.load(map.id, {
loadAs: meOnSecondPeer,
resolve: { list: { $each: { stream: true } } },
});
expectTypeOf(map4).toEqualTypeOf<
| (TestMap & {
@@ -140,8 +150,9 @@ describe("Deep loading with depth arg", async () => {
expect(map4.list[0]?.stream?.[me.id]).not.toBe(null);
expect(map4.list[0]?.stream?.byMe?.value).toBe(null);
const map5 = await TestMap.load(map.id, meOnSecondPeer, {
list: [{ stream: [{}] }],
const map5 = await TestMap.load(map.id, {
loadAs: meOnSecondPeer,
resolve: { list: { $each: { stream: { $each: true } } } },
});
type ExpectedMap5 =
| (TestMap & {
@@ -198,8 +209,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 & {
@@ -222,8 +235,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 & {
@@ -285,9 +300,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 & {
@@ -304,3 +322,53 @@ test("Deep loading a record-like coMap", async () => {
expect(recordLoaded.key2?.list).not.toBe(null);
expect(recordLoaded.key2?.list).not.toBe(undefined);
});
test("The resolve type doesn't accept extra keys", async () => {
const me = await CustomAccount.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto,
});
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;
};
})
| undefined
>();
if (meLoaded === undefined) {
throw new Error("meLoaded is undefined");
}
expect(meLoaded.profile.stream).not.toBe(null);
expect(meLoaded.root.list).not.toBe(null);
});

View File

@@ -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");
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");
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 },
(value: BaseWidget) => {
if (value instanceof BlueButtonWidget) {
expect(value.label).toBe(currentValue);

View File

@@ -8,11 +8,7 @@ import {
Group,
co,
} from "../index.web.js";
import {
type DepthsIn,
createCoValueObservable,
subscribeToCoValue,
} from "../internal.js";
import { createCoValueObservable, subscribeToCoValue } from "../internal.js";
import { setupAccount, waitFor } from "./utils.js";
class ChatRoom extends CoMap {
@@ -53,8 +49,7 @@ describe("subscribeToCoValue", () => {
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{},
{ loadAs: meOnSecondPeer },
updateFn,
);
@@ -114,9 +109,11 @@ describe("subscribeToCoValue", () => {
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [],
loadAs: meOnSecondPeer,
resolve: {
messages: true,
},
},
updateFn,
);
@@ -153,9 +150,13 @@ describe("subscribeToCoValue", () => {
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [{}],
loadAs: meOnSecondPeer,
resolve: {
messages: {
$each: true,
},
},
},
updateFn,
);
@@ -198,13 +199,15 @@ describe("subscribeToCoValue", () => {
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [
{
reactions: [],
loadAs: meOnSecondPeer,
resolve: {
messages: {
$each: {
reactions: true,
},
},
],
},
},
updateFn,
);
@@ -264,13 +267,15 @@ describe("subscribeToCoValue", () => {
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [
{
reactions: [],
loadAs: meOnSecondPeer,
resolve: {
messages: {
$each: {
reactions: true,
},
},
],
},
},
updateFn,
);
@@ -321,14 +326,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();
},
@@ -349,14 +355,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();
},

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

@@ -9,8 +9,10 @@ import {
CoValue,
CoValueClass,
DeeplyLoaded,
DepthsIn,
ID,
RefsToResolve,
RefsToResolveStrict,
Resolved,
subscribeToCoValue,
} from "jazz-tools";
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -49,16 +51,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>;
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): {
me: ComputedRef<Resolved<Acc, R> | undefined>;
logOut: () => void;
};
function useAccount<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<Acc | DeeplyLoaded<Acc, D> | undefined>;
function useAccount<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): {
me: ComputedRef<Acc | Resolved<Acc, R> | undefined>;
logOut: () => void;
} {
const context = useJazzContext();
@@ -75,16 +77,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 BrowserContext<Acc>).me)
: me.value;
@@ -97,17 +99,15 @@ 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 | AnonymousJazzAgent>;
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): {
me: ComputedRef<Resolved<Acc, R> | undefined | AnonymousJazzAgent>;
};
function useAccountOrGuest<D extends DepthsIn<Acc>>(
depth?: D,
): {
me: ComputedRef<
Acc | DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent
>;
function useAccountOrGuest<const R extends RefsToResolve<Acc>>(options?: {
resolve?: RefsToResolveStrict<Acc, R>;
}): {
me: ComputedRef<Acc | Resolved<Acc, R> | undefined | AnonymousJazzAgent>;
} {
const context = useJazzContext();
@@ -119,16 +119,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 BrowserContext<Acc>).me)
: me.value,
),
@@ -151,13 +151,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<V> | undefined>,
depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
): Ref<DeeplyLoaded<V, D> | undefined> {
const state: ShallowRef<DeeplyLoaded<V, D> | undefined> =
shallowRef(undefined);
options?: { resolve?: RefsToResolveStrict<V, R> },
): Ref<Resolved<V, R> | undefined> {
const state: ShallowRef<Resolved<V, R> | undefined> = shallowRef(undefined);
const context = useJazzContext();
if (!context.value) {
@@ -167,7 +166,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();
@@ -177,10 +176,13 @@ 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),
},
(value) => {
state.value = value;
},

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

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

View File

@@ -3,7 +3,7 @@ import { Form } from "./Form.tsx";
import { Logo } from "./Logo.tsx";
function App() {
const { me, logOut } = useAccount({ profile: {}, root: {} });
const { me, logOut } = useAccount({ resolve: { profile: true, root: true } });
return (
<>

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,25 +2,27 @@ 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) => {
function subscribe() {
subscribeToCoValue(
coMap,
valueId,
account,
depth,
options,
(value, unsubscribe) => {
if (predicate(value)) {
resolve(value);

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