Compare commits

..

14 Commits

Author SHA1 Message Date
Anselm
4ee2cad39e Publish
- jazz-example-pets@0.0.17
 - jazz-example-todo@0.0.42
 - jazz-example-twit@0.0.4
 - cojson@0.4.5
 - cojson-simple-sync@0.4.5
 - cojson-storage-indexeddb@0.4.5
 - cojson-storage-sqlite@0.4.5
 - jazz-autosub@0.4.5
 - jazz-browser@0.4.5
 - jazz-browser-auth-local@0.4.5
 - jazz-browser-media-images@0.4.5
 - jazz-react@0.4.5
 - jazz-react-auth-local@0.4.5
2023-10-01 12:18:35 +01:00
Anselm
b7c8a0038b Rename syncedQueries -> autosub and clean up a lot of APIs 2023-10-01 12:16:56 +01:00
Anselm
8c27e8c379 doc fixes 2023-09-28 23:05:59 +01:00
Anselm Eickhoff
0133aa47ff Merge pull request #105 from gardencmp/example-twit
Twitter example
2023-09-28 11:51:38 +01:00
Anselm
5659c925a2 Change Twit html title 2023-09-28 11:44:44 +01:00
Anselm
27779ac792 Publish
- jazz-example-pets@0.0.16
 - jazz-example-todo@0.0.41
 - jazz-example-twit@0.0.3
 - cojson@0.4.1
 - cojson-simple-sync@0.4.1
 - cojson-storage-indexeddb@0.4.1
 - cojson-storage-sqlite@0.4.1
 - jazz-browser@0.4.1
 - jazz-browser-auth-local@0.4.1
 - jazz-browser-media-images@0.4.1
 - jazz-react@0.4.1
 - jazz-react-auth-local@0.4.1
2023-09-28 11:25:36 +01:00
Anselm
3f1bfa4629 Improve twit example 2023-09-28 11:25:09 +01:00
Anselm
15a693c3ed Simplify QueriedCoStream 2023-09-28 11:23:23 +01:00
Anselm
b1d620e145 Update docs 2023-09-28 11:23:06 +01:00
Anselm
478fbd0aa9 Bigger inputs on mobile 2023-09-27 22:21:19 +01:00
Anselm
ee906b7351 Add QR code to own profile 2023-09-27 22:13:55 +01:00
Anselm
dd15f21ccb Fix follow button 2023-09-27 21:51:20 +01:00
Anselm
d7cd5fda7c Actually deploy twit example 2023-09-27 21:43:07 +01:00
Anselm
174300b00f Deploy twit example 2023-09-27 21:39:30 +01:00
85 changed files with 13368 additions and 4356 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
example: ["todo", "pets"]
example: ["todo", "pets", "twit"]
steps:
- uses: actions/checkout@v3
@@ -58,7 +58,7 @@ jobs:
needs: build
strategy:
matrix:
example: ["todo", "pets"]
example: ["todo", "pets", "twit"]
steps:
- uses: actions/checkout@v3

10044
DOCS.md

File diff suppressed because it is too large Load Diff

View File

@@ -56,19 +56,19 @@ Jazz is open source and you can run your own sync & storage server, but to reall
### Building a new, entirely sync-based React app
1. Define your data model with [cojson Collaborative Values (CoValues)](./DOCS.md/#covalue).
2. Implement permission logic using [cojson Groups](./DOCS.md/#group).
3. Build a user interface with [jazz-react](./DOCS.md/#jazz-react)'s reactive [synced queries](./DOCS.md/#usesyncedqueryid).
1. Define your data model with [cojson Collaborative Values (CoValues)](./DOCS.md#covalue).
2. Implement permission logic using [cojson Groups](./DOCS.md#group).
3. Build a user interface with [jazz-react](./DOCS.md#jazz-react) and [auto-sub](./DOCS.md#useautosubid).
### Gradually adding sync to an existing React app
Gradually migrate app features to use sync:
1. Define data model for small aspect of your app with [cojson Collaborative Values (CoValues)](./DOCS.md/#covalue).
1. Define data model for small aspect of your app with [cojson Collaborative Values (CoValues)](./DOCS.md#covalue).
- Schema adapters/importers for Prisma/Drizzle/PostgreSQL introspection coming soon.
2. Map existing permission logic with [cojson Groups](./DOCS.md/#group) & integrate existing auth.
2. Map existing permission logic with [cojson Groups](./DOCS.md#group) & integrate existing auth.
- Auth integrations coming soon.
3. Replace some of the React state and API requests in your UI with [jazz-react](./DOCS.md/#jazz-react)'s reactive [synced queries](./DOCS.md/#usesyncedqueryid).
3. Replace some of the React state and API requests in your UI with [jazz-react](./DOCS.md#jazz-react) and [auto-sub](./DOCS.md#useautosubid).
# Example Apps
@@ -103,10 +103,10 @@ Demonstrates:
For now, docs are hosted in a single well-structured markdown file: [`./DOCS.md`](./DOCS.md).
- [Package Overview](./DOCS.md/#overview)
- [`jazz-react` API](./DOCS.md/#jazz-react)
- [`cojson` API](./DOCS.md/#cojson)
- [`jazz-react-media-images` API](./DOCS.md/#jazz-react-media-images)
- [Package Overview](./DOCS.md#overview)
- [`jazz-react` API](./DOCS.md#jazz-react)
- [`cojson` API](./DOCS.md#cojson)
- [`jazz-browser-media-images` API](./DOCS.md#jazz-browser-media-images)
In the future we'll build a dedicated docs page on the Jazz homepage.

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.15",
"version": "0.0.17",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,9 +16,9 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "^0.4.0",
"jazz-react": "^0.4.0",
"jazz-react-auth-local": "^0.4.0",
"jazz-browser-media-images": "^0.4.5",
"jazz-react": "^0.4.5",
"jazz-react-auth-local": "^0.4.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,10 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import {
Link,
RouterProvider,
createHashRouter,
} from "react-router-dom";
import { Link, RouterProvider, createHashRouter } from "react-router-dom";
import "./index.css";
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
@@ -100,19 +96,19 @@ export function PostOverview() {
return (
<>
root: {JSON.stringify(me.root?.coMap.asObject())}
posts: {JSON.stringify(me.root?.posts?.coList?.asArray())}
<h1>My posts</h1>
{myPosts?.length
? myPosts.map(
(post) =>
post && (
<Link key={post.id} to={"/pet/" + post.id}>
{post.name}
</Link>
)
)
: undefined}
{myPosts?.length ? (
<>
<h1>My posts</h1>
{myPosts.map(
(post) =>
post && (
<Link key={post.id} to={"/pet/" + post.id}>
{post.name}
</Link>
)
)}
</>
) : undefined}
<Link to="/new">New post</Link>
</>
);

View File

@@ -2,7 +2,7 @@ import { ChangeEvent, useCallback, useState } from "react";
import { useNavigate } from "react-router";
import { CoID, CoMap, Media, Profile } from "cojson";
import { useJazz, useSyncedQuery } from "jazz-react";
import { useAutoSub, useJazz } from "jazz-react";
import { BrowserImage, createImage } from "jazz-browser-media-images";
import { PetAccountRoot, PetPost, PetReactions } from "./1_types";
@@ -26,7 +26,7 @@ export function NewPetPostForm() {
CoID<PartialPetPost> | undefined
>(undefined);
const newPetPost = useSyncedQuery(newPostId);
const newPetPost = useAutoSub(newPostId);
const onChangeName = useCallback(
(name: string) => {
@@ -51,7 +51,7 @@ export function NewPetPostForm() {
const image = await createImage(
event.target.files[0],
newPetPost.group
newPetPost.meta.group
);
newPetPost.set({ image: image.id });

View File

@@ -1,6 +1,5 @@
import { useParams } from "react-router";
import { CoID, Queried } from "cojson";
import { useSyncedQuery } from "jazz-react";
import { CoID } from "cojson";
import { PetPost, ReactionType, REACTION_TYPES, PetReactions } from "./1_types";
@@ -8,6 +7,7 @@ import { ShareButton } from "./components/ShareButton";
import { Button, Skeleton } from "./basicComponents";
import { BrowserImage } from "jazz-browser-media-images";
import uniqolor from "uniqolor";
import { Resolved, useAutoSub } from "jazz-react";
/** Walkthrough: TODO
*/
@@ -24,7 +24,7 @@ const reactionEmojiMap: { [reaction in ReactionType]: string } = {
export function RatePetPostUI() {
const petPostID = useParams<{ petPostId: CoID<PetPost> }>().petPostId;
const petPost = useSyncedQuery(petPostID);
const petPost = useAutoSub(petPostID);
return (
<div className="flex flex-col gap-8">
@@ -63,7 +63,7 @@ export function RatePetPostUI() {
))}
</div>
{petPost?.group.myRole() === "admin" && petPost.reactions && (
{petPost?.meta.group.myRole() === "admin" && petPost.reactions && (
<ReactionOverview petReactions={petPost.reactions} />
)}
</div>
@@ -73,16 +73,16 @@ export function RatePetPostUI() {
function ReactionOverview({
petReactions,
}: {
petReactions: Queried<PetReactions>;
petReactions: Resolved<PetReactions>;
}) {
return (
<div>
<h2>Reactions</h2>
<div className="flex flex-col gap-1">
{REACTION_TYPES.map((reactionType) => {
const reactionsOfThisType = Object.values(
petReactions.perAccount
).filter(({ last }) => last === reactionType);
const reactionsOfThisType = petReactions.perAccount
.map(([, reaction]) => reaction)
.filter(({ last }) => last === reactionType);
if (reactionsOfThisType.length === 0) return null;

View File

@@ -2,18 +2,17 @@ import { useState } from "react";
import { PetPost } from "../1_types";
import { createInviteLink } from "jazz-react";
import { Resolved, createInviteLink } from "jazz-react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
import { Queried } from "cojson";
export function ShareButton({ petPost }: { petPost?: Queried<PetPost> }) {
export function ShareButton({ petPost }: { petPost?: Resolved<PetPost> }) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
petPost?.group.myRole() === "admin" && (
petPost?.meta.group.myRole() === "admin" && (
<Button
size="sm"
className="py-0"

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.40",
"version": "0.0.42",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.4.0",
"jazz-react-auth-local": "^0.4.0",
"jazz-react": "^0.4.5",
"jazz-react-auth-local": "^0.4.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -9,10 +9,9 @@ import { SubmittableInput } from "./basicComponents";
import { useNavigate } from "react-router";
export function NewProjectForm() {
// A `LocalNode` represents a local view of loaded & created CoValues.
// It is associated with a current user account, which will determine
// `me` represents the current user account, which will determine
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
const { localNode } = useJazz();
const { me } = useJazz();
const navigate = useNavigate();
const createProject = useCallback(
@@ -22,7 +21,7 @@ export function NewProjectForm() {
// To create a new todo project, we first create a `Group`,
// which is a scope for defining access rights (reader/writer/admin)
// of its members, which will apply to all CoValues owned by that group.
const projectGroup = localNode.createGroup();
const projectGroup = me.createGroup();
// Then we create an empty todo project within that group
const project = projectGroup.createMap<TodoProject>({
@@ -32,7 +31,7 @@ export function NewProjectForm() {
navigate("/project/" + project.id);
},
[localNode, navigate]
[me, navigate]
);
return (

View File

@@ -1,7 +1,6 @@
import { useCallback } from "react";
import { CoID, Queried } from "cojson";
import { useSyncedQuery } from "jazz-react";
import { CoID } from "cojson";
import { TodoProject, Task } from "./1_types";
@@ -20,11 +19,12 @@ import {
import { InviteButton } from "./components/InviteButton";
import uniqolor from "uniqolor";
import { useParams } from "react-router";
import { Resolved, useAutoSub } from "jazz-react";
/** Walkthrough: Reactively rendering a todo project as a table,
* adding and editing tasks
*
* Here in `<TodoTable/>`, we use `useSyncedQuery()` for the first time,
* Here in `<TodoTable/>`, we use `useAutoSub()` for the first time,
* in this case to load the CoValue for our `TodoProject` as well as
* the `ListOfTasks` referenced in it.
*/
@@ -32,11 +32,11 @@ import { useParams } from "react-router";
export function ProjectTodoTable() {
const projectId = useParams<{ projectId: CoID<TodoProject> }>().projectId;
// `useSyncedQuery()` reactively subscribes to updates to a CoValue's
// `useAutoSub()` reactively subscribes to updates to a CoValue's
// content - whether we create edits locally, load persisted data, or receive
// sync updates from other devices or participants!
// It also recursively resolves and subsribes to all referenced CoValues.
const project = useSyncedQuery(projectId);
const project = useAutoSub(projectId);
// `createTask` is similar to `createProject` we saw earlier, creating a new CoMap
// for a new task (in the same group as the project), and then
@@ -44,17 +44,17 @@ export function ProjectTodoTable() {
const createTask = useCallback(
(text: string) => {
if (!project?.tasks || !text) return;
const task = project.group.createMap<Task>({
const task = project.meta.group.createMap<Task>({
done: false,
text,
});
// project.tasks is immutable, but `append` will create an edit
// that will cause useSyncedQuery to rerender this component
// that will cause useAutoSub to rerender this component
// - here and on other devices!
project.tasks.append(task.id);
},
[project?.tasks, project?.group]
[project?.tasks, project?.meta.group]
);
return (
@@ -97,7 +97,7 @@ export function ProjectTodoTable() {
);
}
export function TaskRow({ task }: { task: Queried<Task> | undefined }) {
export function TaskRow({ task }: { task: Resolved<Task> | undefined }) {
return (
<TableRow>
<TableCell>
@@ -124,12 +124,12 @@ export function TaskRow({ task }: { task: Queried<Task> | undefined }) {
{
// Here we see for the first time how we can access edit history
// for a CoValue, and use it to display who created the task.
task?.edits.text?.by?.profile?.name ? (
task?.meta.edits.text?.by?.profile?.name ? (
<span
className="rounded-full py-0.5 px-2 text-xs"
style={uniqueColoring(task.edits.text.by.id)}
style={uniqueColoring(task.meta.edits.text.by.id)}
>
{task.edits.text.by.profile.name}
{task.meta.edits.text.by.profile.name}
</span>
) : (
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />

View File

@@ -1,25 +1,25 @@
import { useState } from "react";
import { createInviteLink } from "jazz-react";
import QRCode from "qrcode";
import { useToast, Button } from "../basicComponents";
import { CoValue, Queried } from "cojson";
import { CoValue } from "cojson";
import { Resolved, createInviteLink } from "jazz-react";
export function InviteButton<T extends CoValue>({ value }: { value: T | Queried<T> | undefined }) {
export function InviteButton<T extends CoValue>({ value }: { value?: Resolved<T> }) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
value?.group?.myRole() === "admin" && (
value?.meta.group?.myRole() === "admin" && (
<Button
size="sm"
className="py-0"
disabled={!value.group || !value.id}
disabled={!value.meta.group || !value.id}
variant="outline"
onClick={async () => {
let inviteLink = existingInviteLink;
if (value.group && value.id && !inviteLink) {
if (value.meta.group && value.id && !inviteLink) {
inviteLink = createInviteLink(value, "writer");
setExistingInviteLink(inviteLink);
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Todo List Example</title>
<title>Twit</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,4 +1,4 @@
job "example-todo$BRANCH_SUFFIX" {
job "twit$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
@@ -41,7 +41,7 @@ job "example-todo$BRANCH_SUFFIX" {
service {
tags = ["public"]
name = "example-todo$BRANCH_SUFFIX"
name = "twit$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-twit",
"private": true,
"version": "0.0.2",
"version": "0.0.4",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,15 +11,16 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"javascript-time-ago": "^2.5.9",
"jazz-browser-media-images": "^0.4.0",
"jazz-react": "^0.4.0",
"jazz-react-auth-local": "^0.4.0",
"jazz-browser-media-images": "^0.4.5",
"jazz-react": "^0.4.5",
"jazz-react-auth-local": "^0.4.5",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",

View File

@@ -1,34 +1,31 @@
import { CoMap, CoList, Media, CoStream, Group, ProfileMeta, AccountMigration, EVERYONE } from 'cojson';
import { CoMap, CoList, Media, CoStream, Group, AccountMigration, EVERYONE, Profile } from 'cojson';
export type Twit = CoMap<{
text?: string;
images?: ListOfImages['id'];
likes: LikeStream['id'];
quotedPost?: Twit['id'];
replies: ReplyStream['id'];
isRepostedIn: RepostedInStream['id'];
isReplyTo?: Twit['id'];
}>;
export type ListOfImages = CoList<Media.ImageDefinition['id']>;
export type LikeStream = CoStream<'❤️' | null>;
export type ReplyStream = CoStream<Twit['id']>;
export type RepostedInStream = CoStream<Twit['id']>;
export type ListOfTwits = CoList<Twit['id']>;
export type ListOfProfiles = CoList<TwitProfile['id']>;
export type StreamOfFollowers = CoStream<TwitProfile['id'] | null>;
export type TwitProfile = CoMap<
export type TwitProfile = Profile<
{
name: string;
bio: string;
avatar?: Media.ImageDefinition['id'];
twits: ListOfTwits['id'];
following: ListOfProfiles['id'];
followers: ListOfProfiles['id'];
followers: StreamOfFollowers['id'];
twitStyle?: { fontFamily: string; color: string };
},
ProfileMeta
}
>;
export type TwitAccountRoot = CoMap<{
@@ -61,7 +58,7 @@ export const migration: AccountMigration<TwitProfile, TwitAccountRoot> = (accoun
profile.set('twits', peopleWhoCanSeeMyTwits.createList<ListOfTwits>().id, 'trusting');
profile.set('following', peopleWhoCanSeeMyFollows.createList<ListOfProfiles>().id, 'trusting');
profile.set('followers', peopleWhoCanFollowMe.createList<ListOfProfiles>().id, 'trusting');
profile.set('followers', peopleWhoCanFollowMe.createStream<StreamOfFollowers>().id, 'trusting');
console.log('MIGRATION SUCCESSFUL!');
}
};

View File

@@ -1,49 +1,18 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Link, RouterProvider, createHashRouter, useParams } from 'react-router-dom';
import { RouterProvider, createHashRouter } from 'react-router-dom';
import './index.css';
import { WithJazz, useJazz, useSyncedQuery } from 'jazz-react';
import { AccountMigration } from 'cojson';
import { WithJazz, useJazz } from 'jazz-react';
import { LocalAuth } from 'jazz-react-auth-local';
import {
AddTwitPicsInput,
BioInput,
Button,
ButtonWithCount,
ChooseProfilePicInput,
FollowerStatsContainer,
LargeProfilePicImg,
ProfileName,
ProfilePicImg,
ProfileTitleContainer,
ReactionsAndReplyContainer,
ReactionsContainer,
RepliesContainer,
SubtleProfileID,
SubtleRelativeTimeAgo,
ThemeProvider,
TitleAndLogo,
TwitImg,
TwitTextInput
} from './basicComponents/index.tsx';
import { Button, ThemeProvider, TitleAndLogo } from './basicComponents/index.tsx';
import { PrettyAuthUI } from './components/Auth.tsx';
import {
LikeStream,
ListOfImages,
ReplyStream,
RepostedInStream,
Twit,
TwitAccountRoot,
TwitProfile,
migration
} from './1_types.ts';
import { AccountMigration, CoID, Queried } from 'cojson';
import { BrowserImage, createImage } from 'jazz-browser-media-images';
import TimeAgo from 'javascript-time-ago';
import en from 'javascript-time-ago/locale/en.json';
import { HeartIcon, MessagesSquareIcon } from 'lucide-react';
TimeAgo.addDefaultLocale(en);
import { migration } from './1_dataModel.ts';
import { ChronoFeed } from './3_ChronoFeed.tsx';
import { ProfilePage } from './5_ProfilePage.tsx';
const appName = 'Jazz Twit Example';
@@ -65,30 +34,21 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
</React.StrictMode>
);
/**
* Routing in `<App/>`
*
* <App> is the main app component, handling client-side routing based
* on the CoValue ID (CoID) of our TodoProject, stored in the URL hash
* - which can also contain invite links.
*/
function App() {
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
const { logOut } = useJazz();
const { me, logOut } = useJazz();
const router = createHashRouter([
{
path: '/',
element: <ChronoFeedUI />
},
{
path: '/me',
element: <MyProfile />
element: <ChronoFeed />
},
{
path: '/:profileId',
element: <UserProfile />
element: <ProfilePage />
},
{
path: '/me',
loader: () => router.navigate('/' + me.profile?.id)
}
]);
@@ -109,283 +69,3 @@ function App() {
</>
);
}
export function MyProfile() {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
return me.profile && <ProfileUI profileId={me.profile.id} />;
}
export function UserProfile() {
const { profileId } = useParams<{ profileId: CoID<TwitProfile> }>();
return profileId && <ProfileUI profileId={profileId} />;
}
export function ProfileUI({ profileId }: { profileId: CoID<TwitProfile> }) {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const profile = useSyncedQuery(profileId);
const isMe = profile?.id == me.profile?.id;
const profileTwitsAndRepliedToTwits = useMemo(() => {
return profile?.twits?.map((twit, _, allTwits) =>
twit?.isReplyTo
? allTwits.some(
tw =>
tw?.id === twit?.isReplyTo?.id ||
tw?.id === twit?.isReplyTo?.isReplyTo?.id ||
tw?.id === twit?.isReplyTo?.isReplyTo?.isReplyTo?.id
)
? null
: twit?.isReplyTo
: twit
);
}, [profile?.twits]);
return (
<div>
<div className="py-2 mb-5 flex gap-2">
<div className="flex flex-col items-stretch">
<LargeProfilePicImg src={profile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder} />
{isMe && (
<ChooseProfilePicInput
onChange={(file: File) =>
me.root?.peopleWhoCanSeeMyTwits &&
createImage(file, me.root.peopleWhoCanSeeMyTwits.group, 256).then(image => {
me.profile?.set({ avatar: image.id }, 'trusting');
})
}
/>
)}
</div>
<div className="grow">
<ProfileTitleContainer>
<ProfileName>{profile?.name}</ProfileName>
<div className="ml-2 text-neutral-300 text-xs">{profile?.id}</div>
{!isMe && (
<Button
onClick={() => {
if (!profile?.followers || !me.profile?.following) return;
if (profile.followers.some(f => f?.id === me.profile?.id)) {
me.profile.following.append(profile.id);
profile.followers.append(me.profile.id);
} else {
me.profile.following.delete(me.profile.following.findIndex(f => f?.id === profile.id));
profile.followers.delete(profile.followers.findIndex(f => f?.id === me.profile?.id));
}
}}
className="ml-auto"
>
{profile?.followers?.some(f => f?.id === me.profile?.id) ? 'Unfollow' : 'Follow'}
</Button>
)}
</ProfileTitleContainer>
<div>
{isMe ? (
<BioInput
value={profile?.bio}
onChange={newBio => {
profile?.set({ bio: newBio }, 'trusting');
// prettier-ignore
if (newBio.startsWith('{')) {profile?.set('twitStyle', JSON.parse(newBio), 'trusting');} else {profile?.set('twitStyle', undefined, 'trusting');}
}}
/>
) : (
profile?.bio || '(No bio)'
)}
</div>
<FollowerStatsContainer>
{new Set(profile?.followers || []).size} Followers &mdash; {new Set(profile?.following || []).size}{' '}
Following
</FollowerStatsContainer>
</div>
</div>
{isMe && <CreateTwitForm className="mb-4" />}
{profileTwitsAndRepliedToTwits?.map(twit => twit && <TwitUI twit={twit} key={twit?.id} />)}
</div>
);
}
export function TwitUI({
twit,
alreadyInReplies: alreadyInReplies
}: {
twit?: Queried<Twit>;
alreadyInReplies?: boolean;
}) {
const [showReplyForm, setShowReplyForm] = React.useState(false);
const posterProfile = twit?.edits.text?.by?.profile as Queried<TwitProfile> | undefined;
return (
<div
className={'py-2 flex flex-col items-stretch' + (twit?.isReplyTo && !alreadyInReplies ? ' ml-14' : ' border-t')}
>
<div className="flex gap-2">
<ProfilePicImg
src={posterProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
smaller={!!twit?.isReplyTo}
/>
<div className="grow flex flex-col items-stretch">
<div className="flex items-baseline">
{posterProfile && (
<Link to={'/' + posterProfile.id} className="font-bold">
{posterProfile.name}
</Link>
)}
<SubtleProfileID>{posterProfile?.id}</SubtleProfileID>
<SubtleRelativeTimeAgo dateTime={twit?.edits.text?.at} />
</div>
<div style={posterProfile?.twitStyle}>{twit?.text}</div>
{twit?.quotedPost && (
<div className="border rounded">
<TwitUI twit={twit.quotedPost} />
</div>
)}
{twit?.images && (
<div className="flex gap-2 mt-2 max-w-full overflow-auto">
{twit.images.map(image => (
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id} />
))}
</div>
)}
<ReactionsAndReplyContainer>
<ReactionsContainer>
<ButtonWithCount
active={twit?.likes?.me?.last === '❤️'}
onClick={() => twit?.likes?.push(twit?.likes?.me?.last ? null : '❤️')}
count={Object.values(twit?.likes?.perAccount || {}).filter(byAccount => byAccount.last === '❤️').length}
icon={<HeartIcon size="18" />}
activeIcon={<HeartIcon color="red" size="18" fill="red" />}
/>
<ButtonWithCount
onClick={() => setShowReplyForm(s => !s)}
count={Object.values(twit?.replies?.perSession || {}).flatMap(perSession => perSession.all).length}
icon={<MessagesSquareIcon size="18" />}
/>
</ReactionsContainer>
</ReactionsAndReplyContainer>
{showReplyForm && (
<CreateTwitForm inReplyTo={twit} onSubmit={() => setShowReplyForm(false)} className="mt-5" />
)}
</div>
</div>
<RepliesContainer>
{Object.values(twit?.replies?.perAccount || {})
.flatMap(byAccount => byAccount.all)
.sort((a, b) => b.at.getTime() - a.at.getTime())
.map(replyEntry => (
<TwitUI twit={replyEntry.value} key={replyEntry.value?.id} alreadyInReplies={!!twit?.isReplyTo} />
))}
</RepliesContainer>
</div>
);
}
export function ChronoFeedUI() {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const myTwits = me.profile?.twits;
const twitsFromFollows = useMemo(
() => me.profile?.following?.flatMap(follow => follow?.twits || []) || [],
[me.profile?.following]
);
const allTwitsSorted = useMemo(
() =>
[...(myTwits || []), ...twitsFromFollows]
.flatMap(tw => (tw ? (tw.isReplyTo ? [] : tw) : []))
.sort((a, b) => (b.edits.text?.at?.getTime() || 0) - (a.edits.text?.at?.getTime() || 0)),
[myTwits, twitsFromFollows]
);
return (
<div className="flex flex-col items-stretch">
<CreateTwitForm className="mb-10" />
<h1 className="text-2xl mb-4">From people you follow</h1>
{allTwitsSorted?.map(twit => (
<TwitUI twit={twit} key={twit.id} />
))}
</div>
);
}
export function CreateTwitForm(
props: {
inReplyTo?: Queried<Twit>;
onSubmit?: () => void;
className?: string;
} = {}
) {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const [pics, setPics] = React.useState<File[]>([]);
const onSubmit = useCallback(
(twitText: string) => {
const audience = me.root?.peopleWhoCanSeeMyTwits;
const interactors = me.root?.peopleWhoCanInteractWithMe;
if (!audience || !interactors) return;
const images = pics.length ? audience.createList<ListOfImages>() : undefined;
const twit = audience.createMap<Twit>({
text: twitText,
likes: interactors.createStream<LikeStream>().id,
replies: interactors.createStream<ReplyStream>().id,
isRepostedIn: interactors.createStream<RepostedInStream>().id,
images: images?.id
});
me.profile?.twits?.prepend(twit?.id as Twit['id']);
if (props.inReplyTo) {
props.inReplyTo.replies?.push(twit.id);
twit.set({ isReplyTo: props.inReplyTo.id });
}
pics.forEach(pic => {
createImage(pic, twit.group, 1024).then(image => {
images!.append(image.id);
});
});
setPics([]);
props.onSubmit?.();
},
[me.profile?.twits, me.root?.peopleWhoCanSeeMyTwits, me.root?.peopleWhoCanInteractWithMe, props, pics]
);
const [picPreviews, setPicPreviews] = React.useState<string[]>([]);
useEffect(() => {
const previews = pics.map(pic => URL.createObjectURL(pic));
setPicPreviews(previews);
return () => previews.forEach(preview => URL.revokeObjectURL(preview));
}, [pics]);
return (
<div className={props.className}>
<TwitTextInput onSubmit={onSubmit} submitButtonLabel={props.inReplyTo ? 'Reply!' : 'Twit!'} />
{picPreviews.length ? (
<div className="flex gap-2 mt-2">
{picPreviews.map(preview => (
<TwitImg src={preview} />
))}
</div>
) : (
<AddTwitPicsInput
onChange={(newPics: File[]) => {
setPics(newPics);
}}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { useJazz } from 'jazz-react';
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
import { TwitComponent } from './4_TwitComponent.tsx';
import { MainH1 } from './basicComponents/index.tsx';
export function ChronoFeed() {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const myTwits = me.profile?.twits;
const twitsFromFollows = useMemo(
() => me.profile?.following?.flatMap(follow => follow?.twits || []) || [],
[me.profile?.following]
);
const allTwitsSorted = useMemo(
() =>
[...(myTwits || []), ...twitsFromFollows]
.flatMap(tw => (tw ? (tw.isReplyTo ? [] : tw) : []))
.sort((a, b) => (b.meta.edits.text?.at?.getTime() || 0) - (a.meta.edits.text?.at?.getTime() || 0)),
[myTwits, twitsFromFollows]
);
return (
<div className="flex flex-col items-stretch">
<CreateTwitForm className="mb-10" />
<MainH1>From people you follow</MainH1>
{allTwitsSorted?.map(twit => (
<TwitComponent twit={twit} key={twit.id} />
))}
</div>
);
}

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Link } from 'react-router-dom';
import {
ButtonWithCount,
ProfilePicImg,
ReactionsContainer,
RepliesContainer,
SubtleRelativeTimeAgo,
TwitContainer,
TwitWithRepliesContainer,
TwitImg,
TwitImgGallery,
TwitHeader,
TwitBody,
TwitText,
} from './basicComponents/index.tsx';
import { Twit, TwitProfile } from './1_dataModel.ts';
import { BrowserImage } from 'jazz-browser-media-images';
import { HeartIcon, MessagesSquareIcon } from 'lucide-react';
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
import { Resolved } from 'jazz-react';
export function TwitComponent({
twit,
alreadyInReplies: alreadyInReplies
}: {
twit?: Resolved<Twit>;
alreadyInReplies?: boolean;
}) {
const [showReplyForm, setShowReplyForm] = React.useState(false);
const posterProfile = twit?.meta.edits.text?.by?.profile as Resolved<TwitProfile> | undefined;
const isTopLevel = !twit?.isReplyTo || alreadyInReplies;
return (
<TwitWithRepliesContainer isTopLevel={isTopLevel}>
<TwitContainer>
<ProfilePicImg
src={posterProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
linkTo={'/' + posterProfile?.id}
initial={posterProfile?.name[0]}
size={twit?.isReplyTo && "sm"}
/>
<TwitBody>
<TwitHeader>
<Link to={'/' + posterProfile?.id} className="font-bold hover:underline">
{posterProfile?.name}
</Link>
<SubtleRelativeTimeAgo dateTime={twit?.meta.edits.text?.at} />
</TwitHeader>
<TwitText style={posterProfile?.twitStyle}>
{/* This is where the tweet text goes */}
{twit?.text}
</TwitText>
{twit?.images && (
<TwitImgGallery>
{twit.images.map(image => (
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id} />
))}
</TwitImgGallery>
)}
<ReactionsContainer>
<ButtonWithCount
active={twit?.likes?.me?.last === '❤️'}
onClick={() => twit?.likes?.push(twit?.likes?.me?.last ? null : '❤️')}
count={twit?.likes?.perAccount.filter(([, liked]) => liked.last === '❤️').length || 0}
icon={<HeartIcon size="18" />}
activeIcon={<HeartIcon color="red" size="18" fill="red" />}
/>
<ButtonWithCount
onClick={() => setShowReplyForm(s => !s)}
count={twit?.replies?.perAccount.flatMap(([, byAccount]) => byAccount.all).length || 0}
icon={<MessagesSquareIcon size="18" />}
/>
</ReactionsContainer>
</TwitBody>
</TwitContainer>
<RepliesContainer>
{showReplyForm && (
<CreateTwitForm
inReplyTo={twit}
onSubmit={() => setShowReplyForm(false)}
className={'mt-5 ' + (isTopLevel ? 'ml-14' : 'ml-12')}
/>
)}
{twit?.replies?.perAccount
.flatMap(([, byAccount]) => byAccount.all)
.sort((a, b) => b.at.getTime() - a.at.getTime())
.map(replyEntry => (
<TwitComponent twit={replyEntry.value} key={replyEntry.value?.id} alreadyInReplies={!!twit?.isReplyTo} />
))}
</RepliesContainer>
</TwitWithRepliesContainer>
);
}

View File

@@ -0,0 +1,127 @@
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useJazz, useAutoSub } from 'jazz-react';
import QRCode from 'qrcode';
import {
BioInput,
ChooseProfilePicInput,
FollowerStatsContainer,
Popover,
ProfileName,
ProfilePicImg,
ProfileTitleContainer,
SmallInlineButton
} from './basicComponents/index.tsx';
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { CoID } from 'cojson';
import { BrowserImage, createImage } from 'jazz-browser-media-images';
import { FollowButton, FollowerList, FollowingList } from './7_FollowStuff.tsx';
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
import { TwitComponent } from './4_TwitComponent.tsx';
import { PopoverContent, PopoverTrigger } from '@radix-ui/react-popover';
export function ProfilePage() {
const { profileId } = useParams<{ profileId: CoID<TwitProfile> }>();
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const profile = useAutoSub(profileId);
const isMe = profile?.id == me.profile?.id;
const profileTwitsAndRepliedToTwits = useMemo(() => {
return profile?.twits?.map((twit, _, allTwits) =>
twit?.isReplyTo
? allTwits.some(
tw =>
tw?.id === twit?.isReplyTo?.id ||
tw?.id === twit?.isReplyTo?.isReplyTo?.id ||
tw?.id === twit?.isReplyTo?.isReplyTo?.isReplyTo?.id
)
? null
: twit?.isReplyTo
: twit
);
}, [profile?.twits]);
const [qr, setQr] = useState<string>('');
useEffect(() => {
QRCode.toDataURL(
window.location.protocol + '//' + window.location.host + window.location.pathname + '#/' + profile?.id,
{
errorCorrectionLevel: 'L'
}
).then(setQr);
}, [profile?.id]);
return (
<div>
<div className="py-2 mb-5 flex gap-4">
<div className="flex flex-col items-stretch">
<ProfilePicImg
src={profile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
initial={profile?.name[0]}
size="xxl"
/>
{isMe && (
<ChooseProfilePicInput
onChange={(file: File) =>
me.root?.peopleWhoCanSeeMyTwits &&
createImage(file, me.root.peopleWhoCanSeeMyTwits, 256).then(image => {
me.profile?.set({ avatar: image.id }, 'trusting');
})
}
/>
)}
</div>
<div className="grow">
<ProfileTitleContainer>
<ProfileName>{profile?.name}</ProfileName>
{!isMe && <FollowButton profile={profile} />}
</ProfileTitleContainer>
<div>
{isMe ? (
<BioInput
value={profile?.bio}
onChange={newBio => {
profile?.set({ bio: newBio }, 'trusting');
// prettier-ignore
if (newBio.startsWith('{')) { profile?.set('twitStyle', JSON.parse(newBio), 'trusting'); } else { profile?.set('twitStyle', undefined, 'trusting'); }
}}
/>
) : (
profile?.bio || '(No bio)'
)}
</div>
<FollowerStatsContainer>
<Popover>
<PopoverTrigger>
<SmallInlineButton>
{profile?.followers?.perAccount?.filter(([, status]) => status.last).length} Followers
</SmallInlineButton>
</PopoverTrigger>
<PopoverContent>
<FollowerList profile={profile} />
</PopoverContent>
</Popover>
<span className="hidden md:block">&mdash;</span> <br className="md:hidden" />
<Popover>
<PopoverTrigger>
<SmallInlineButton>{new Set(profile?.following || []).size} Following</SmallInlineButton>
</PopoverTrigger>
<PopoverContent>
<FollowingList profile={profile} />
</PopoverContent>
</Popover>
</FollowerStatsContainer>
</div>
{isMe && <img src={qr} className="rounded w-28 h-28 -mr-3 dark:invert max-sm:w-16 max-sm:h-16" />}
</div>
{isMe && <CreateTwitForm className="mb-4" />}
{profileTwitsAndRepliedToTwits?.map(twit => twit && <TwitComponent twit={twit} key={twit?.id} />)}
</div>
);
}

View File

@@ -0,0 +1,73 @@
import React, { useCallback, useEffect } from 'react';
import { Resolved, useJazz } from 'jazz-react';
import { AddTwitPicsInput, TwitImg, TwitTextInput } from './basicComponents/index.tsx';
import { LikeStream, ListOfImages, ReplyStream, Twit, TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { createImage } from 'jazz-browser-media-images';
export function CreateTwitForm(
props: {
inReplyTo?: Resolved<Twit>;
onSubmit?: () => void;
className?: string;
} = {}
) {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const [pics, setPics] = React.useState<File[]>([]);
const onSubmit = useCallback(
(twitText: string) => {
const audience = me.root?.peopleWhoCanSeeMyTwits;
const interactors = me.root?.peopleWhoCanInteractWithMe;
if (!audience || !interactors) return;
const twit = audience.createMap<Twit>({
text: twitText,
likes: interactors.createStream<LikeStream>().id,
replies: interactors.createStream<ReplyStream>().id
});
me.profile?.twits?.prepend(twit?.id as Twit['id']);
if (props.inReplyTo) {
props.inReplyTo.replies?.push(twit.id);
twit.set({ isReplyTo: props.inReplyTo.id });
}
Promise.all(pics.map(pic => createImage(pic, twit.group, 1024))).then(createdPics => {
twit.set({ images: audience.createList<ListOfImages>(createdPics.map(pic => pic.id)).id });
});
setPics([]);
props.onSubmit?.();
},
[me.profile?.twits, me.root?.peopleWhoCanSeeMyTwits, me.root?.peopleWhoCanInteractWithMe, props, pics]
);
const [picPreviews, setPicPreviews] = React.useState<string[]>([]);
useEffect(() => {
const previews = pics.map(pic => URL.createObjectURL(pic));
setPicPreviews(previews);
return () => previews.forEach(preview => URL.revokeObjectURL(preview));
}, [pics]);
return (
<div className={props.className}>
<TwitTextInput onSubmit={onSubmit} submitButtonLabel={props.inReplyTo ? 'Reply!' : 'Twit!'} />
{picPreviews.length ? (
<div className="flex gap-2 mt-2">
{picPreviews.map(preview => (
<TwitImg src={preview} />
))}
</div>
) : (
<AddTwitPicsInput
onChange={(newPics: File[]) => {
setPics(newPics);
}}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,80 @@
import { useCallback } from 'react';
import { Resolved, useJazz } from 'jazz-react';
import { Button, ProfilePicImg } from './basicComponents/index.tsx';
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
import { Link } from 'react-router-dom';
import { BrowserImage } from 'jazz-browser-media-images';
export function FollowButton({ profile }: { profile?: Resolved<TwitProfile> }) {
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
const alreadyFollowing = profile?.followers?.perAccount?.some(([acc, status]) => acc === me.id && !!status.last);
const theyFollowMe = profile?.following?.some(f => f?.id === me.profile?.id);
const followOrUnfollow = useCallback(() => {
if (!profile?.followers || !me.profile?.following) return;
if (alreadyFollowing) {
me.profile.following.delete(me.profile.following.findIndex(f => f?.id === profile.id));
profile.followers.push(null);
} else {
me.profile.following.append(profile.id);
profile.followers.push(me.profile.id);
}
}, [alreadyFollowing, me.profile, profile]);
return profile?.id === me.profile?.id ? (
<div className="ml-auto text-neutral-500">That's you!</div>
) : (
<Button onClick={followOrUnfollow} className="ml-auto" variant={alreadyFollowing ? 'ghost' : 'default'}>
{alreadyFollowing ? 'Unfollow' : theyFollowMe ? 'Follow Back' : 'Follow'}
</Button>
);
}
export function FollowerList({ profile }: { profile?: Resolved<TwitProfile> }) {
return (
<div className="flex flex-col gap-4 p-4 bg-background rounded-lg border shadow-lg w-96 max-w-full m-2">
{profile?.followers?.perAccount.map(([, followEntry]) => {
const followerProfile = followEntry.last;
// not following anymore?
if (!followerProfile) return null;
return (
<div key={followerProfile.id} className="flex items-center">
<ProfilePicImg
src={followerProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
linkTo={'/' + followerProfile?.id}
initial={followerProfile?.name[0]}
/>
<Link to={'/' + followerProfile?.id} className="font-bold hover:underline">
{followerProfile?.name}
</Link>
<FollowButton profile={followerProfile} />
</div>
);
})}
</div>
);
}
export function FollowingList({ profile }: { profile?: Resolved<TwitProfile> }) {
return (
<div className="flex flex-col gap-4 p-4 bg-background rounded-lg border shadow-lg w-96 max-w-full m-2">
{[...new Set(profile?.following || [])].map(followingProfile => {
return (
<div key={followingProfile?.id} className="flex items-center">
<ProfilePicImg
src={followingProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
linkTo={'/' + followingProfile?.id}
initial={followingProfile?.name[0]}
/>
<Link to={'/' + followingProfile?.id} className="font-bold hover:underline">
{followingProfile?.name}
</Link>
<FollowButton profile={followingProfile} />
</div>
);
})}
</div>
);
}

View File

@@ -1,8 +1,9 @@
import ReactTimeAgo from 'react-time-ago';
import { Button } from './ui/button';
import { Button, ButtonProps } from './ui/button';
export { Button } from './ui/button';
export { Checkbox } from './ui/checkbox';
import { Input } from './ui/input';
import { Link } from 'react-router-dom';
export { Input } from './ui/input';
export { Skeleton } from './ui/skeleton';
export { Toaster } from './ui/toaster';
@@ -12,6 +13,11 @@ export { TitleAndLogo } from './TitleAndLogo';
export { ThemeProvider } from './themeProvider';
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
export { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import TimeAgo from 'javascript-time-ago';
import en from 'javascript-time-ago/locale/en.json';
TimeAgo.addDefaultLocale(en);
export function BioInput(props: { value?: string; onChange: (value: string) => void }) {
return (
@@ -23,7 +29,7 @@ export function BioInput(props: { value?: string; onChange: (value: string) => v
props.onChange(e.target.value);
}}
placeholder="Add a bio..."
className="w-full p-2 border rounded"
className="w-full p-2 border rounded max-md:text-base"
/>
);
}
@@ -59,23 +65,35 @@ export function ChooseProfilePicInput(props: { onChange: (file: File) => void })
);
}
export function LargeProfilePicImg(props: { src?: string }) {
return <img src={props.src} className="w-20 h-20 bg-neutral-200 rounded-full mr-2 object-cover" />;
}
export function ProfilePicImg(props: { src?: string; smaller?: boolean }) {
export function ProfilePicImg(props: { src?: string; size?: 'sm' | 'xxl'; linkTo?: string; initial?: string }) {
return (
<img
src={props.src}
className={'bg-neutral-200 rounded-full mr-2 object-cover shrink-0' + (props.smaller ? ' w-8 h-8' : ' w-10 h-10')}
/>
<Link to={props.linkTo || ''}>
{props.src ? (
<img
src={props.src}
className={
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0' +
(props.size === 'sm' ? ' w-8 h-8' : props.size === 'xxl' ? ' w-20 h-20' : ' w-10 h-10')
}
/>
) : (
<div
className={
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0 flex items-center justify-center text-neutral-700 ' +
(props.size === 'sm'
? ' w-8 h-8 text-[1.5rem]'
: props.size === 'xxl'
? ' w-20 h-20 text-[3.75rem]'
: ' w-10 h-10 text-[1.875rem]')
}
>
<div className="-mt-[8%]">{props.initial}</div>
</div>
)}
</Link>
);
}
export function SubtleProfileID(props: { children: React.ReactNode }) {
return <div className="ml-2 text-neutral-300 text-xs">{props.children}</div>;
}
export function SubtleRelativeTimeAgo(props: { dateTime?: Date }) {
return (
<div className="ml-auto text-neutral-300 text-xs whitespace-nowrap">
@@ -88,12 +106,8 @@ export function TwitImg(props: { src?: string }) {
return <img src={props.src} className="h-40 rounded object-cover" />;
}
export function ReactionsAndReplyContainer(props: { children: React.ReactNode }) {
return <div className="flex flex-col mt-2">{props.children}</div>;
}
export function ReactionsContainer(props: { children: React.ReactNode }) {
return <div className="flex gap-4">{props.children}</div>;
return <div className="flex gap-4 mt-2">{props.children}</div>;
}
export function RepliesContainer(props: { children: React.ReactNode }) {
@@ -140,7 +154,7 @@ export function TwitTextInput(props: { onSubmit: (text: string) => void; submitB
name="twitText"
placeholder="What's happenin'"
autoComplete="off"
className="p-2 border rounded grow"
className="p-2 border rounded grow max-md:text-base"
/>
<Button asChild>
<input type="submit" value={props.submitButtonLabel} />
@@ -167,3 +181,48 @@ export function AddTwitPicsInput(props: { onChange: (files: File[]) => void }) {
</Button>
);
}
export function TwitWithRepliesContainer(props: { children: React.ReactNode; isTopLevel?: boolean }) {
return (
<div className={'py-2 flex flex-col items-stretch' + (props.isTopLevel ? ' border-t' : ' ml-14')}>
{props.children}
</div>
);
}
export function TwitContainer(props: { children: React.ReactNode }) {
return <div className="flex gap-2">{props.children}</div>;
}
export function TwitBody(props: { children: React.ReactNode }) {
return <div className="grow flex flex-col items-stretch">{props.children}</div>;
}
export function TwitHeader(props: { children: React.ReactNode }) {
return <div className="flex items-baseline">{props.children}</div>;
}
export function TwitImgGallery(props: { children: React.ReactNode }) {
return <div className="flex gap-2 mt-2 max-w-full overflow-auto">{props.children}</div>;
}
export function TwitText(props: { children: React.ReactNode; style?: React.CSSProperties }) {
return <div style={props.style}>{props.children}</div>;
}
export function QuoteContainer(props: { children: React.ReactNode }) {
return <div className="border rounded">{props.children}</div>;
}
export function MainH1(props: { children: React.ReactNode }) {
return <h1 className="text-2xl mb-4">{props.children}</h1>;
}
export function SmallInlineButton(props: { children: React.ReactNode } & ButtonProps) {
const {children, ...rest} = props
return (
<Button variant={'ghost'} className="h-6 px-1 -mx-1" {...rest}>
{children}
</Button>
);
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/basicComponents/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -12,6 +12,7 @@ async function main() {
cojson: "index.ts",
"jazz-browser": "index.ts",
"jazz-browser-media-images": "index.ts",
"jazz-autosub": "index.ts",
}).map(async ([packageName, entryPoint]) => {
const app = await Application.bootstrapWithPlugins({
entryPoints: [`packages/${packageName}/src/${entryPoint}`],

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
homepage/homepage-jazz/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

View File

@@ -0,0 +1,22 @@
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,113 @@
import Image from 'next/image'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore the Next.js 13 playground.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}

View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig

View File

@@ -0,0 +1,27 @@
{
"name": "homepage-jazz",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "latest",
"react-dom": "latest",
"next": "latest"
},
"devDependencies": {
"typescript": "latest",
"@types/react": "latest",
"@types/node": "latest",
"@types/react-dom": "latest",
"autoprefixer": "latest",
"postcss": "latest",
"tailwindcss": "latest",
"eslint": "latest",
"eslint-config-next": "latest"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,20 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default config

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -4,7 +4,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.4.0",
"version": "0.4.5",
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/ws": "^8.5.5",
@@ -16,8 +16,8 @@
"typescript": "5.0.2"
},
"dependencies": {
"cojson": "^0.4.0",
"cojson-storage-sqlite": "^0.4.0",
"cojson": "^0.4.5",
"cojson-storage-sqlite": "^0.4.5",
"ws": "^8.13.0"
},
"scripts": {

View File

@@ -1,11 +1,11 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.4.0",
"cojson": "^0.4.5",
"typescript": "^5.1.6"
},
"devDependencies": {
@@ -16,7 +16,7 @@
"scripts": {
"test": "vitest --browser chrome",
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"

View File

@@ -1,18 +1,18 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "^0.4.0",
"cojson": "^0.4.5",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"devDependencies": {

View File

@@ -2,10 +2,10 @@
"name": "cojson",
"module": "dist/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.4.0",
"version": "0.4.5",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",
@@ -26,7 +26,7 @@
"scripts": {
"test": "jest",
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"jest": {

View File

@@ -1,11 +1,7 @@
import { JsonObject, JsonValue } from "./jsonValue.js";
import { RawCoID } from "./ids.js";
import { CoMap } from "./coValues/coMap.js";
import {
BinaryCoStream,
BinaryCoStreamMeta,
CoStream,
} from "./coValues/coStream.js";
import { BinaryCoStream, CoStream } from "./coValues/coStream.js";
import { CoList } from "./coValues/coList.js";
import { CoValueCore } from "./coValueCore.js";
import { Group } from "./coValues/group.js";
@@ -22,7 +18,7 @@ export interface CoValue {
/** Specifies which kind of `CoValue` this is */
type: string;
/** The `CoValue`'s (precisely typed) static metadata */
meta: JsonObject | null;
headerMeta: JsonObject | null;
/** The `Group` this `CoValue` belongs to (determining permissions) */
group: Group;
/** Returns an immutable JSON presentation of this `CoValue` */
@@ -38,7 +34,14 @@ export interface CoValue {
subscribe(listener: (coValue: this) => void): () => void;
}
export type AnyCoValue = CoMap | Group | Account | Profile | CoList | CoStream | BinaryCoStream;
export type AnyCoValue =
| CoMap
| Group
| Account
| Profile
| CoList
| CoStream
| BinaryCoStream;
export function expectMap(content: CoValue): CoMap {
if (content.type !== "comap") {

View File

@@ -197,7 +197,7 @@ export class CoListView<
}
/** @category 6. Meta */
get meta(): Meta {
get headerMeta(): Meta {
return this.core.header.meta as Meta;
}

View File

@@ -84,7 +84,7 @@ export class CoMapView<
}
/** @category 6. Meta */
get meta(): Meta {
get headerMeta(): Meta {
return this.core.header.meta as Meta;
}

View File

@@ -4,7 +4,7 @@ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
import { Group } from "./group.js";
import { AgentID, SessionID, TransactionID } from "../ids.js";
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
import { AccountID } from "./account.js";
import { AccountID, isAccountID } from "./account.js";
import { parseJSON } from "../jsonStringify.js";
export type BinaryStreamInfo = {
@@ -59,7 +59,7 @@ export class CoStreamView<
this.fillFromCoValue();
}
get meta(): Meta {
get headerMeta(): Meta {
return this.core.header.meta as Meta;
}
@@ -111,8 +111,8 @@ export class CoStreamView<
return Object.keys(this.items) as SessionID[];
}
accounts(): Set<AccountID | AgentID> {
return new Set(this.sessions().map(accountOrAgentIDfromSessionID));
accounts(): Set<AccountID> {
return new Set(this.sessions().map(accountOrAgentIDfromSessionID).filter(isAccountID));
}
nthItemIn(
@@ -290,8 +290,6 @@ export class BinaryCoStreamView<
extends CoStreamView<BinaryStreamItem, Meta>
implements CoValue
{
id!: CoID<this>;
getBinaryChunks(
allowUnfinished?: boolean
):

View File

@@ -277,7 +277,7 @@ export class Group<
*/
createMap<M extends CoMap>(
init?: M["_shape"],
meta?: M["meta"],
meta?: M["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): M {
let map = this.core.node
@@ -309,7 +309,7 @@ export class Group<
*/
createList<L extends CoList>(
init?: L["_item"][],
meta?: L["meta"],
meta?: L["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): L {
let list = this.core.node
@@ -334,7 +334,7 @@ export class Group<
}
/** @category 3. Value creation */
createStream<C extends CoStream>(meta?: C["meta"]): C {
createStream<C extends CoStream>(meta?: C["headerMeta"]): C {
return this.core.node
.createCoValue({
type: "costream",
@@ -350,7 +350,7 @@ export class Group<
/** @category 3. Value creation */
createBinaryStream<C extends BinaryCoStream>(
meta: C["meta"] = { type: "binary" }
meta: C["headerMeta"] = { type: "binary" }
): C {
return this.core.node
.createCoValue({

View File

@@ -2,7 +2,7 @@ import {
CoValueCore,
newRandomSessionID,
MAX_RECOMMENDED_TX_SIZE,
accountOrAgentIDfromSessionID
accountOrAgentIDfromSessionID,
} from "./coValueCore.js";
import { LocalNode } from "./localNode.js";
import type { CoValue } from "./coValue.js";
@@ -30,20 +30,16 @@ import {
AnonymousControlledAccount,
ControlledAccount,
} from "./coValues/account.js";
import type { Role } from "./permissions.js";
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
import { Group, expectGroup, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import { Account, Profile, isAccountID } from "./coValues/account.js";
import type { SessionID, AgentID } from "./ids.js";
import type { CoID, AnyCoValue } from "./coValue.js";
import type { Queried, QueryExtension } from "./queries.js";
import type { QueriedCoStream } from "./queriedCoValues/queriedCoStream.js";
import type { QueriedCoList } from "./queriedCoValues/queriedCoList.js";
import type { QueriedCoMap } from "./queriedCoValues/queriedCoMap.js";
import { QueriedAccount } from "./queriedCoValues/queriedAccount.js";
import { QueriedGroup } from "./queriedCoValues/queriedGroup.js";
import type {
BinaryStreamInfo,
BinaryCoStreamMeta,
@@ -51,7 +47,12 @@ import type {
import type { JsonValue } from "./jsonValue.js";
import type { SyncMessage, Peer } from "./sync.js";
import type { AgentSecret } from "./crypto.js";
import type { AccountID, AccountMeta, AccountMigration, ProfileMeta } from "./coValues/account.js";
import type {
AccountID,
AccountMeta,
AccountMigration,
ProfileMeta,
} from "./coValues/account.js";
import type { InviteSecret } from "./coValues/group.js";
import type * as Media from "./media.js";
@@ -82,7 +83,9 @@ export const cojsonInternals = {
export {
LocalNode,
Group,
Role,
EVERYONE,
Everyone,
CoMap,
MutableCoMap,
CoList,
@@ -94,12 +97,6 @@ export {
CoValue,
CoID,
AnyCoValue,
Queried,
QueriedCoMap,
QueriedCoList,
QueriedCoStream,
QueriedGroup,
QueriedAccount,
Account,
AccountID,
AccountMeta,
@@ -113,7 +110,6 @@ export {
ControlledAccount,
cryptoReady as cojsonReady,
MAX_RECOMMENDED_TX_SIZE,
Value,
JsonValue,
Peer,
BinaryStreamInfo,
@@ -122,9 +118,12 @@ export {
AgentSecret,
InviteSecret,
SyncMessage,
QueryExtension,
};
export type {
Value,
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CojsonInternalTypes {
export type CoValueKnownState = import("./sync.js").CoValueKnownState;
@@ -134,6 +133,7 @@ export namespace CojsonInternalTypes {
export type NewContentMessage = import("./sync.js").NewContentMessage;
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
export type Transaction = import("./coValueCore.js").Transaction;
export type TransactionID = import("./ids.js").TransactionID;
export type Signature = import("./crypto.js").Signature;
export type RawCoID = import("./ids.js").RawCoID;
export type ProfileShape = import("./coValues/account.js").ProfileShape;

View File

@@ -17,14 +17,12 @@ import {
import {
InviteSecret,
Group,
GroupShape,
expectGroup,
secretSeedFromInviteSecret,
} from "./coValues/group.js";
import { Peer, SyncManager } from "./sync.js";
import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
import { CoID } from "./coValue.js";
import { Queried, query } from "./queries.js";
import {
Account,
AccountMeta,
@@ -34,12 +32,10 @@ import {
AnonymousControlledAccount,
AccountID,
Profile,
isAccountID,
AccountMigration,
} from "./coValues/account.js";
import { CoMap } from "./coValues/coMap.js";
import { CoValue } from "./index.js";
import { QueriedAccount } from "./queriedCoValues/queriedAccount.js";
/** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
@@ -103,7 +99,8 @@ export class LocalNode {
newRandomSessionID(account.id)
);
const accountOnNodeWithAccount = nodeWithAccount.account as ControlledAccount<P, R, Meta>;
const accountOnNodeWithAccount =
nodeWithAccount.account as ControlledAccount<P, R, Meta>;
const profile = nodeWithAccount.expectProfileLoaded(
accountOnNodeWithAccount.id,
@@ -256,47 +253,6 @@ export class LocalNode {
};
}
/** @category 1. High-level */
query<T extends CoValue>(
id: CoID<T>,
callback: (update: Queried<T> | undefined) => void
): () => void;
query<
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
>(
id: "me",
callback: (
update: QueriedAccount<Account<P, R, Meta>> | undefined
) => void
): () => void;
query(
id: CoID<CoValue> | "me",
callback: (
update: Queried<CoValue> | QueriedAccount | undefined
) => void
): () => void;
query(
id: CoID<CoValue> | "me",
callback: (
// TODO: sort this out
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update: any
) => void
): () => void {
if (id === "me") {
const meId = this.account.id;
if (!isAccountID(meId)) {
throw new Error("Can only query 'me' for accounts");
}
return query(meId, this, callback);
} else {
return query(id, this, callback);
}
}
/** @deprecated Use Account.acceptInvite instead */
async acceptInvite<T extends CoValue>(
groupOrOwnedValueID: CoID<T>,
@@ -601,7 +557,10 @@ export class LocalNode {
if (account instanceof ControlledAccount) {
// To make sure that when we edit the account, we're modifying the correct sessions
const accountInNode = new ControlledAccount(newNode.expectCoValueLoaded(account.id), account.agentSecret);
const accountInNode = new ControlledAccount(
newNode.expectCoValueLoaded(account.id),
account.agentSecret
);
if (accountInNode.core.node !== newNode) {
throw new Error("Account's node is not the new node");
}

View File

@@ -1,240 +0,0 @@
import { CoList, MutableCoList } from "../coValues/coList.js";
import { CoValueCore } from "../coValueCore.js";
import { Group } from "../coValues/group.js";
import { CoID, CoValue } from "../coValue.js";
import { TransactionID } from "../ids.js";
import { ValueOrSubQueried, QueryContext } from "../queries.js";
import { QueriedAccount } from "./queriedAccount.js";
export class QueriedCoList<L extends CoList> extends Array<
ValueOrSubQueried<L["_item"]>
> {
coList!: L;
id!: CoID<L>;
type!: "colist";
/** @internal */
constructor(coList: L, queryContext: QueryContext) {
if (!(coList instanceof CoList)) {
// this might be called from an intrinsic, like map, trying to create an empty array
// passing `0` as the only parameter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Array(coList) as any;
}
super(
...coList
.asArray()
.map(
(item) =>
queryContext.queryIfCoID(item, [coList.id]) as ValueOrSubQueried<
L["_item"]
>
)
);
Object.defineProperties(this, {
coList: { get() {return coList} },
id: { value: coList.id },
type: { value: "colist" },
edits: {
value: [...this.keys()].map((i) => {
const edit = coList.editAt(i)!;
return queryContext.defineSubqueryPropertiesIn({
tx: edit.tx,
at: new Date(edit.at),
}, {
by: {value: edit.by, enumerable: true},
value: {value: edit.value, enumerable: true},
}, [coList.id]);
}),
},
deletions: {
value: coList.deletionEdits().map((deletion) => queryContext.defineSubqueryPropertiesIn({
tx: deletion.tx,
at: new Date(deletion.at),
}, {
by: {value: deletion.by, enumerable: true},
}, [coList.id])),
},
});
}
get meta(): L["meta"] {
return this.coList.meta;
}
get group(): Group {
return this.coList.group;
}
get core(): CoValueCore {
return this.coList.core;
}
append(
item: L["_item"],
after?: number,
privacy?: "private" | "trusting"
): L {
return this.coList.append(item, after, privacy);
}
prepend(
item: L["_item"],
before?: number,
privacy?: "private" | "trusting"
): L {
return this.coList.prepend(item, before, privacy);
}
delete(at: number, privacy?: "private" | "trusting"): L {
return this.coList.delete(at, privacy);
}
mutate(
mutator: (mutable: MutableCoList<L["_item"], L["meta"]>) => void
): L {
return this.coList.mutate(mutator);
}
edits!: {
by?: QueriedAccount;
tx: TransactionID;
at: Date;
value: L["_item"] extends CoValue
? CoID<L["_item"]>
: Exclude<L["_item"], CoValue>;
}[];
deletions!: {
by?: QueriedAccount;
tx: TransactionID;
at: Date;
}[];
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isArray(arg: any): arg is any[] {
return Array.isArray(arg);
}
/** @internal */
static from<T>(arrayLike: ArrayLike<T>): T[];
/** @internal */
static from<T, U>(
arrayLike: ArrayLike<T>,
mapfn: (v: T, k: number) => U,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
thisArg?: any
): U[];
/** @internal */
static from<T>(iterable: Iterable<T> | ArrayLike<T>): T[];
/** @internal */
static from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapfn: (v: T, k: number) => U,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
thisArg?: any
): U[];
/** @internal */
static from<T, U>(
_iterable: unknown,
_mapfn?: unknown,
_thisArg?: unknown
): T[] | U[] | T[] | U[] {
throw new Error("Array method 'from' not supported on QueriedCoList");
}
/** @internal */
static of<T>(..._items: T[]): T[] {
throw new Error("Array method 'of' not supported on QueriedCoList");
}
/** @internal */
pop(): ValueOrSubQueried<L["_item"]> | undefined {
throw new Error("Array method 'pop' not supported on QueriedCoList");
}
/** @internal */
push(..._items: ValueOrSubQueried<L["_item"]>[]): number {
throw new Error("Array method 'push' not supported on QueriedCoList");
}
/** @internal */
concat(
..._items: ConcatArray<ValueOrSubQueried<L["_item"]>>[]
): ValueOrSubQueried<L["_item"]>[];
/** @internal */
concat(
..._items: (
| ValueOrSubQueried<L["_item"]>
| ConcatArray<ValueOrSubQueried<L["_item"]>>
)[]
): ValueOrSubQueried<L["_item"]>[];
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
concat(..._items: any[]): ValueOrSubQueried<L["_item"]>[] {
throw new Error("Array method 'concat' not supported on QueriedCoList");
}
/** @internal */
reverse(): ValueOrSubQueried<L["_item"]>[] {
throw new Error(
"Array method 'reverse' not supported on QueriedCoList"
);
}
/** @internal */
shift(): ValueOrSubQueried<L["_item"]> | undefined {
throw new Error("Array method 'shift' not supported on QueriedCoList");
}
/** @internal */
sort(
_compareFn?:
| ((
a: ValueOrSubQueried<L["_item"]>,
b: ValueOrSubQueried<L["_item"]>
) => number)
| undefined
): this {
throw new Error("Array method 'sort' not supported on QueriedCoList");
}
/** @internal */
splice(
_start: number,
_deleteCount?: number | undefined
): ValueOrSubQueried<L["_item"]>[] {
throw new Error("Array method 'splice' not supported on QueriedCoList");
}
/** @internal */
unshift(..._items: ValueOrSubQueried<L["_item"]>[]): number {
throw new Error(
"Array method 'unshift' not supported on QueriedCoList"
);
}
/** @internal */
fill(
_value: ValueOrSubQueried<L["_item"]>,
_start?: number | undefined,
_end?: number | undefined
): this {
throw new Error("Array method 'fill' not supported on QueriedCoList");
}
/** @internal */
copyWithin(
_target: number,
_start: number,
_end?: number | undefined
): this {
throw new Error(
"Array method 'copyWithin' not supported on QueriedCoList"
);
}
}

View File

@@ -1,168 +0,0 @@
import { CoMap, MutableCoMap } from "../coValues/coMap.js";
import { CoValueCore } from "../coValueCore.js";
import { Group } from "../coValues/group.js";
import { CoID } from "../coValue.js";
import { TransactionID } from "../ids.js";
import { ValueOrSubQueried, QueryContext, QueryExtension } from "../queries.js";
import { QueriedAccount } from "./queriedAccount.js";
export type QueriedCoMap<M extends CoMap> = {
[K in keyof M["_shape"] & string]: ValueOrSubQueried<M["_shape"][K]>;
} & QueriedCoMapBase<M>;
export type QueriedCoMapEdit<M extends CoMap, K extends keyof M["_shape"]> = {
by?: QueriedAccount;
tx: TransactionID;
at: Date;
value: M["_shape"][K];
};
export class QueriedCoMapBase<M extends CoMap> {
coMap!: M;
id!: CoID<M>;
type!: "comap";
/** @internal */
static newWithKVPairs<M extends CoMap>(
coMap: M,
queryContext: QueryContext
): QueriedCoMap<M> {
const kv = {} as {
[K in keyof M["_shape"] & string]: ValueOrSubQueried<
M["_shape"][K]
>;
};
for (const key of coMap.keys()) {
const value = coMap.get(key);
if (value === undefined) continue;
queryContext.defineSubqueryPropertiesIn(
kv,
{
[key]: { value, enumerable: true },
},
[coMap.id]
);
}
return Object.assign(new QueriedCoMapBase(coMap, queryContext), kv);
}
/** @internal */
constructor(coMap: M, queryContext: QueryContext) {
Object.defineProperties(this, {
coMap: {
get() {
return coMap;
},
enumerable: false,
},
id: { value: coMap.id, enumerable: false },
type: { value: "comap", enumerable: false },
edits: {
value: Object.fromEntries(
coMap.keys().flatMap((key) => {
const edits = [...coMap.editsAt(key)].map((edit) =>
queryContext.defineSubqueryPropertiesIn(
{
tx: edit.tx,
at: new Date(edit.at),
},
{
by: { value: edit.by, enumerable: true },
value: {
value: edit.value,
enumerable: true,
},
},
[coMap.id]
)
);
const lastEdit = edits[edits.length - 1];
if (!lastEdit) return [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const editsAtKey = {
by: lastEdit.by,
tx: lastEdit.tx,
at: lastEdit.at,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: lastEdit.value as any,
all: edits,
};
return [[key, editsAtKey]];
})
),
enumerable: false,
},
as: {
value: <O>(extension: QueryExtension<M, O>) => {
return queryContext.getOrCreateExtension(
coMap.id,
extension
);
},
enumerable: false,
},
});
}
edits!: {
[K in keyof M["_shape"] & string]:
| (QueriedCoMapEdit<M, K> & {
all: QueriedCoMapEdit<M, K>[];
})
| undefined;
};
get meta(): M["meta"] {
return this.coMap.meta;
}
get group(): Group {
return this.coMap.group;
}
get core(): CoValueCore {
return this.coMap.core;
}
set<K extends keyof M["_shape"] & string>(
key: K,
value: M["_shape"][K],
privacy?: "private" | "trusting"
): M;
set(
kv: {
[K in keyof M["_shape"] & string]?: M["_shape"][K];
},
privacy?: "private" | "trusting"
): M;
set<K extends keyof M["_shape"] & string>(
...args:
| [
{
[K in keyof M["_shape"] & string]?: M["_shape"][K];
},
("private" | "trusting")?
]
| [K, M["_shape"][K], ("private" | "trusting")?]
): M {
// eslint-disable-next-line @typescript-eslint/ban-types
return (this.coMap.set as Function)(...args);
}
delete(
key: keyof M["_shape"] & string,
privacy?: "private" | "trusting"
): M {
return this.coMap.delete(key, privacy);
}
mutate(
mutator: (mutable: MutableCoMap<M["_shape"], M["meta"]>) => void
): M {
return this.coMap.mutate(mutator);
}
as!: <O>(extension: QueryExtension<M, O>) => O | undefined;
}

View File

@@ -1,155 +0,0 @@
import { JsonValue } from "../jsonValue.js";
import { CoStream, MutableCoStream } from "../coValues/coStream.js";
import { CoValueCore } from "../coValueCore.js";
import { Group } from "../coValues/group.js";
import { AccountID, isAccountID } from "../coValues/account.js";
import { CoID, CoValue } from "../coValue.js";
import { SessionID, TransactionID } from "../ids.js";
import { ValueOrSubQueried, QueryContext } from "../queries.js";
import { QueriedAccount } from "./queriedAccount.js";
export type QueriedCoStreamItems<Item extends JsonValue | CoValue> = {
last?: ValueOrSubQueried<Item>;
by?: QueriedAccount;
tx?: TransactionID;
at?: Date;
all: {
value: ValueOrSubQueried<Item>;
by?: QueriedAccount;
tx: TransactionID;
at: Date;
}[];
};
export class QueriedCoStream<S extends CoStream> {
coStream!: S;
id: CoID<S>;
type = "costream" as const;
/** @internal */
constructor(coStream: S, queryContext: QueryContext) {
Object.defineProperty(this, "coStream", {
get() {
return coStream;
},
});
this.id = coStream.id;
this.perSession = Object.fromEntries(
coStream.sessions().map((sessionID) => {
const items = [...coStream.itemsIn(sessionID)].map((item) =>
queryContext.defineSubqueryPropertiesIn(
{
tx: item.tx,
at: new Date(item.at),
},
{
by: {
value: isAccountID(item.by)
? item.by
: (undefined as never),
enumerable: true,
},
value: {
value: item.value as S["_item"],
enumerable: true,
},
},
[coStream.id]
)
);
const lastItem = items[items.length - 1];
return [
sessionID,
{
get last() {
return lastItem?.value;
},
get by() {
return lastItem?.by;
},
tx: lastItem?.tx,
at: lastItem?.at,
all: items,
} satisfies QueriedCoStreamItems<S["_item"]>,
];
})
);
this.perAccount = Object.fromEntries(
[...coStream.accounts()].map((accountID) => {
const items = [...coStream.itemsBy(accountID)].map((item) => queryContext.defineSubqueryPropertiesIn(
{
tx: item.tx,
at: new Date(item.at),
},
{
by: {
value: isAccountID(item.by)
? item.by
: (undefined as never),
enumerable: true,
},
value: {
value: item.value as S["_item"],
enumerable: true,
},
},
[coStream.id]
));
const lastItem = items[items.length - 1];
return [
accountID,
{
get last() {
return lastItem?.value;
},
get by() {
return lastItem?.by;
},
tx: lastItem?.tx,
at: lastItem?.at,
all: items,
} satisfies QueriedCoStreamItems<S["_item"]>,
];
})
);
this.me = isAccountID(queryContext.node.account.id)
? this.perAccount[queryContext.node.account.id]
: undefined;
}
get meta(): S["meta"] {
return this.coStream.meta;
}
get group(): Group {
return this.coStream.group;
}
get core(): CoValueCore {
return this.coStream.core;
}
me?: QueriedCoStreamItems<S["_item"]>;
perAccount: {
[account: AccountID]: QueriedCoStreamItems<S["_item"]>;
};
perSession: {
[session: SessionID]: QueriedCoStreamItems<S["_item"]>;
};
push(item: S["_item"], privacy?: "private" | "trusting"): S {
return this.coStream.push(item, privacy);
}
mutate(
mutator: (mutable: MutableCoStream<S["_item"], S["meta"]>) => void
): S {
return this.coStream.mutate(mutator);
}
}

View File

@@ -1,90 +0,0 @@
import { Everyone, Group, InviteSecret } from "../coValues/group.js";
import { CoID } from "../coValue.js";
import { QueryContext, ValueOrSubQueried } from "../queries.js";
import { CoValueCore } from "../coValueCore.js";
import { Role } from "../permissions.js";
import { AccountID } from "../coValues/account.js";
import { CoMap } from "../coValues/coMap.js";
import { CoList } from "../coValues/coList.js";
import { CoStream } from "../coValues/coStream.js";
import { BinaryCoStream } from "../coValues/coStream.js";
export class QueriedGroup<G extends Group = Group> {
group!: G;
id!: CoID<G>;
type = "group" as const;
profile?: ValueOrSubQueried<G["_shape"]["profile"]>;
root?: ValueOrSubQueried<G["_shape"]["root"]>;
constructor(group: G, queryContext: QueryContext) {
const profileID = group.get("profile");
const rootID = group.get("root");
queryContext.defineSubqueryPropertiesIn(Object.defineProperties(this, {
group: {
get() {
return group;
},
enumerable: false,
},
id: { value: group.id, enumerable: false },
type: { value: "group", enumerable: false },
}), {
profile: {
value: profileID,
enumerable: false,
},
root: {
value: rootID,
enumerable: false,
},
}, [group.id]);
}
get meta(): G["meta"] {
return this.group.meta;
}
get core(): CoValueCore {
return this.group.core;
}
addMember(accountID: AccountID | Everyone, role: Role): G {
return this.group.addMember(accountID, role);
}
removeMember(accountID: AccountID): G {
return this.group.removeMember(accountID);
}
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {
return this.group.createInvite(role);
}
createMap<M extends CoMap>(
init?: {
[K in keyof M["_shape"]]: M["_shape"][K];
},
meta?: M["meta"],
initPrivacy: "trusting" | "private" = "private"
): M {
return this.group.createMap(init, meta, initPrivacy);
}
createList<L extends CoList>(
init?: L["_item"][],
meta?: L["meta"],
initPrivacy: "trusting" | "private" = "private"
): L {
return this.group.createList(init, meta, initPrivacy);
}
createStream<C extends CoStream>(meta?: C["meta"]): C {
return this.group.createStream(meta);
}
createBinaryStream<C extends BinaryCoStream>(
meta: C["meta"] = { type: "binary" }
): C {
return this.group.createBinaryStream(meta);
}
}

View File

@@ -1,263 +0,0 @@
import { JsonValue } from "./jsonValue.js";
import { CoMap } from "./coValues/coMap.js";
import { CoStream } from "./coValues/coStream.js";
import { CoList } from "./coValues/coList.js";
import { Account, AccountID } from "./coValues/account.js";
import { CoID, CoValue } from "./coValue.js";
import { LocalNode } from "./localNode.js";
import {
QueriedCoMap,
QueriedCoMapBase,
} from "./queriedCoValues/queriedCoMap.js";
import { QueriedCoList } from "./queriedCoValues/queriedCoList.js";
import { QueriedCoStream } from "./queriedCoValues/queriedCoStream.js";
import { Group } from "./coValues/group.js";
import { QueriedAccount } from "./queriedCoValues/queriedAccount.js";
import { QueriedGroup } from "./queriedCoValues/queriedGroup.js";
export type Queried<T extends CoValue> = T extends CoMap
? T extends Account
? QueriedAccount<T>
: T extends Group
? QueriedGroup<T>
: QueriedCoMap<T>
: T extends CoList
? QueriedCoList<T>
: T extends CoStream
? T["meta"] extends { type: "binary" }
? never
: QueriedCoStream<T>
:
| QueriedAccount
| QueriedGroup
| QueriedCoMap<CoMap>
| QueriedCoList<CoList>
| QueriedCoStream<CoStream>;
export type ValueOrSubQueried<
V extends JsonValue | CoValue | CoID<CoValue> | undefined
> = V extends CoID<infer C>
? Queried<C> | undefined
: V extends CoValue
? Queried<V> | undefined
: V;
export interface CleanupCallbackAndUsable {
(): void;
[Symbol.dispose]: () => void;
}
export interface QueryExtension<T extends CoValue, O> {
id: string;
query(
base: T,
queryContext: QueryContext,
onUpdate: (value: O) => void
): () => void;
}
export class QueryContext {
values: {
[id: CoID<CoValue>]: {
lastUpdate: CoValue | undefined;
lastQueried: Queried<CoValue> | undefined;
render: () => void;
unsubscribe: () => void;
};
} = {};
extensions: {
[id: `${CoID<CoValue>}_${string}`]: {
lastOutput: unknown;
unsubscribe: () => void;
};
} = {};
node: LocalNode;
onUpdate: () => void;
constructor(node: LocalNode, onUpdate: () => void) {
this.node = node;
this.onUpdate = onUpdate;
}
query<T extends CoValue>(valueID: CoID<T>, alsoRender: CoID<CoValue>[]) {
let value = this.values[valueID];
if (!value) {
const render = () => {
let newQueried;
const lastUpdate = value!.lastUpdate;
if (lastUpdate instanceof CoMap) {
if (lastUpdate instanceof Account) {
newQueried = new QueriedAccount(
lastUpdate,
this
) as Queried<T>;
} else if (lastUpdate instanceof Group) {
newQueried = new QueriedGroup(
lastUpdate,
this
) as Queried<T>;
} else {
newQueried = QueriedCoMapBase.newWithKVPairs(
lastUpdate,
this
) as Queried<T>;
}
} else if (lastUpdate instanceof CoList) {
newQueried = new QueriedCoList(
lastUpdate,
this
) as Queried<T>;
} else if (lastUpdate instanceof CoStream) {
if (lastUpdate.meta?.type === "binary") {
// Querying binary string not yet implemented
} else {
newQueried = new QueriedCoStream(
lastUpdate,
this
) as Queried<T>;
}
}
// console.log(
// "Rendered ",
// valueID,
// lastUpdate?.constructor.name,
// newQueried
// );
value!.lastQueried = newQueried;
for (const alsoRenderID of alsoRender) {
// console.log("Also rendering", alsoRenderID);
this.values[alsoRenderID]?.render();
}
};
value = {
lastQueried: undefined,
lastUpdate: undefined,
render,
unsubscribe: this.node.subscribe(valueID, (valueUpdate) => {
value!.lastUpdate = valueUpdate;
value!.render();
this.onUpdate();
}),
};
this.values[valueID] = value;
}
return value.lastQueried as Queried<T> | undefined;
}
queryIfCoID<T extends JsonValue | undefined>(value: T, alsoRender: CoID<CoValue>[]): T extends CoID<infer C> ? Queried<C> | undefined : T {
if (typeof value === "string" && value.startsWith("co_")) {
return this.query(value as CoID<CoValue>, alsoRender) as T extends CoID<infer C> ? Queried<C> | undefined : never;
} else {
return value as T extends CoID<infer C> ? Queried<C> | undefined : T;
}
}
valueOrSubQueryPropertyDescriptor<T extends JsonValue | undefined>(
value: T,
alsoRender: CoID<CoValue>[]
): T extends CoID<infer C>
? { get(): Queried<C> | undefined }
: { value: T } {
if (typeof value === "string" && value.startsWith("co_")) {
// TODO: when we track render dirty status, we can actually return the queried value without a getter if it's up to date
return {
get: () => this.query(value as CoID<CoValue>, alsoRender),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { value: value } as any;
}
}
defineSubqueryPropertiesIn<
O extends object,
P extends {
[key: string]: { value: JsonValue | undefined; enumerable: boolean };
}
>(
obj: O,
subqueryProps: P,
alsoRender: CoID<CoValue>[]
): O & {
[Key in keyof P]: ValueOrSubQueried<P[Key]["value"]>;
} {
for (const [key, descriptor] of Object.entries(subqueryProps)) {
Object.defineProperty(
obj,
key,
{
...this.valueOrSubQueryPropertyDescriptor(descriptor.value, alsoRender),
enumerable: descriptor.enumerable,
}
);
}
return obj as O & {
[Key in keyof P]: ValueOrSubQueried<P[Key]["value"]>
};
}
getOrCreateExtension<T extends CoValue, O>(
valueID: CoID<T>,
extension: QueryExtension<T, O>
): O | undefined {
const id = `${valueID}_${extension.id}`;
let ext = this.extensions[id as keyof typeof this.extensions];
if (!ext) {
ext = {
lastOutput: undefined,
unsubscribe: extension.query(
this.node
.expectCoValueLoaded(valueID)
.getCurrentContent() as T,
this,
(output) => {
ext!.lastOutput = output;
this.values[valueID]?.render();
this.onUpdate();
}
),
};
this.extensions[id as keyof typeof this.extensions] = ext;
}
return ext.lastOutput as O | undefined;
}
cleanup() {
for (const child of Object.values(this.values)) {
child.unsubscribe?.();
}
for (const extension of Object.values(this.extensions)) {
extension.unsubscribe();
}
}
}
export function query<T extends CoValue>(
id: CoID<T>,
node: LocalNode,
callback: (queried: Queried<T> | undefined) => void
): CleanupCallbackAndUsable {
// console.log("querying", id);
const context = new QueryContext(node, () => {
const rootQueried = context.values[id]?.lastQueried as
| Queried<T>
| undefined;
callback(rootQueried);
});
context.query(id, []);
const cleanup = function cleanup() {
context.cleanup();
} as CleanupCallbackAndUsable;
cleanup[Symbol.dispose] = cleanup;
return cleanup;
}

View File

@@ -315,14 +315,14 @@ test("Empty BinaryCoStream works", () => {
if (
content.type !== "costream" ||
content.meta?.type !== "binary" ||
content.headerMeta?.type !== "binary" ||
!(content instanceof BinaryCoStream)
) {
throw new Error("Expected binary stream");
}
expect(content.type).toEqual("costream");
expect(content.meta.type).toEqual("binary");
expect(content.headerMeta.type).toEqual("binary");
expect(content.toJSON()).toEqual({});
expect(content.getBinaryChunks()).toEqual(undefined);
});
@@ -341,7 +341,7 @@ test("Can push into BinaryCoStream", () => {
if (
content.type !== "costream" ||
content.meta?.type !== "binary" ||
content.headerMeta?.type !== "binary" ||
!(content instanceof BinaryCoStream)
) {
throw new Error("Expected binary stream");
@@ -398,7 +398,7 @@ test("When adding large transactions (small fraction of MAX_RECOMMENDED_TX_SIZE)
if (
content.type !== "costream" ||
content.meta?.type !== "binary" ||
content.headerMeta?.type !== "binary" ||
!(content instanceof BinaryCoStream)
) {
throw new Error("Expected binary stream");
@@ -474,7 +474,7 @@ test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we s
if (
content.type !== "costream" ||
content.meta?.type !== "binary" ||
content.headerMeta?.type !== "binary" ||
!(content instanceof BinaryCoStream)
) {
throw new Error("Expected binary stream");

View File

@@ -46,6 +46,6 @@ test("Can create a BinaryCoStream in a group", () => {
const stream = group.createBinaryStream();
expect(stream.core.getCurrentContent().type).toEqual("costream");
expect(stream.meta.type).toEqual("binary");
expect(stream.headerMeta.type).toEqual("binary");
expect(stream instanceof BinaryCoStream).toEqual(true);
})

View File

@@ -1,337 +0,0 @@
import {
BinaryCoStream,
CoList,
CoMap,
CoStream,
Group,
LocalNode,
cojsonReady,
} from "..";
beforeEach(async () => {
await cojsonReady;
});
test("Queries with maps work", async () => {
const { node, accountID } =
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
const group = node.createGroup();
let map = group.createMap<
CoMap<{
hello: "world";
subMap: CoMap<{
hello: "world" | "moon" | "sun";
id: string;
}>["id"];
}>
>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(map.id, (queriedMap) => {
// console.log("update", update);
if (queriedMap) {
expect(queriedMap.type).toBe("comap");
expect(queriedMap.id).toEqual(map.id);
expect(queriedMap.core).toEqual(map.core);
expect(queriedMap.group).toBeInstanceOf(Group);
expect(queriedMap.group.id).toBe(group.id);
expect(queriedMap.meta).toBe(null);
expect(queriedMap.hello).toBe("world");
expect(Object.keys(queriedMap)).toEqual(["hello", "subMap"]);
if (queriedMap.edits.hello?.by?.profile?.name) {
expect(queriedMap.edits.hello.by.id).toEqual(accountID);
expect(queriedMap.edits.hello.by.profile.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(queriedMap.edits.hello.by.profile.name).toEqual(
"Hermes Puggington"
);
expect(queriedMap.edits.hello.by.isMe).toBe(true);
expect(queriedMap.edits.hello.tx).toEqual(
map.lastEditAt("hello")!.tx
);
expect(queriedMap.edits.hello.at).toEqual(
new Date(map.lastEditAt("hello")!.at)
);
if (queriedMap.subMap) {
expect(queriedMap.subMap.type).toBe("comap");
expect(queriedMap.subMap.id).toEqual("foreignID");
expect(queriedMap.subMap.core).toEqual(subMap.core);
expect(queriedMap.subMap.group).toBeInstanceOf(Group);
expect(queriedMap.subMap.group.id).toBe(group.id);
expect(queriedMap.subMap.meta).toBe(null);
if (queriedMap.subMap.hello === "moon") {
// console.log("got to 'moon'");
queriedMap.subMap.set("hello", "sun");
} else if (
queriedMap.subMap.hello === "sun" &&
queriedMap.subMap.edits.hello?.by?.profile?.name ===
"Hermes Puggington"
) {
// console.log("final update", queriedMap);
resolve();
unsubQuery();
}
}
}
}
});
});
map = map.set("hello", "world");
let subMap = group.createMap<
CoMap<{
hello: "world" | "moon" | "sun";
id: string;
}>
>();
map = map.set("subMap", subMap.id);
subMap = subMap.mutate((subMap) => {
subMap.set("hello", "world");
subMap.set("id", "foreignID");
});
subMap = subMap.set("hello", "moon");
await done;
});
test("Queries with lists work", () => {
const { node, accountID } =
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
const group = node.createGroup();
let list = group.createList<CoList<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(list.id, (queriedList) => {
if (queriedList) {
// console.log("update", queriedList, queriedList.edits);
expect(queriedList.type).toBe("colist");
expect(queriedList.id).toEqual(list.id);
expect(queriedList.core).toEqual(list.core);
expect(queriedList.group).toBeInstanceOf(Group);
expect(queriedList.group.id).toBe(group.id);
expect(queriedList.meta).toBe(null);
expect(queriedList[0]).toBe("hello");
expect(queriedList[1]).toBe("world");
expect(queriedList[2]).toBe("moon");
if (queriedList.edits[2]?.by?.profile?.name) {
expect(queriedList.edits[2].by.id).toEqual(accountID);
expect(queriedList.edits[2].by.profile.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(queriedList.edits[2].by.profile.name).toEqual(
"Hermes Puggington"
);
expect(queriedList.edits[2].by.isMe).toBe(true);
expect(queriedList.edits[2].at).toBeInstanceOf(Date);
if (queriedList.length === 3) {
queriedList.append("sun");
} else if (
queriedList.length === 4 &&
queriedList.edits[3]?.by?.profile?.name ===
"Hermes Puggington"
) {
expect(queriedList[3]).toBe("sun");
// console.log("final update", queriedList);
resolve();
unsubQuery();
}
}
}
});
});
list = list.mutate((list) => {
list.append("hello");
list.append("world");
list.append("moon");
});
return done;
});
test("List of nested maps works", () => {
const { node } = LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
const group = node.createGroup();
let list = group.createList<CoList<CoMap<{ hello: "world" }>["id"]>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(list.id, (queriedList) => {
if (queriedList && queriedList[0]) {
// console.log("update", queriedList);
expect(queriedList[0]).toMatchObject({
hello: "world",
id: list.get(0)!,
});
// console.log("final update", queriedList);
resolve();
unsubQuery();
}
});
});
list = list.append(
group.createMap<CoMap<{ hello: "world" }>>({
hello: "world",
}).id
);
return done;
});
test("Can call .map on a quieried coList", async () => {
const { node } = LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
const group = node.createGroup();
let list = group.createList<CoList<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(list.id, (queriedList) => {
if (queriedList && queriedList[0]) {
// console.log("update", queriedList);
expect(queriedList.map((item) => item + "!!!")).toEqual([
"hello!!!",
"world!!!",
]);
// console.log("final update", queriedList);
resolve();
unsubQuery();
}
});
});
list = list.mutate((list) => {
list.append("hello");
list.append("world");
});
await done;
});
test("Queries with streams work", () => {
const { node, accountID } =
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
const group = node.createGroup();
let stream = group.createStream<CoStream<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(stream.id, (queriedStream) => {
if (queriedStream) {
// console.log("update", queriedStream);
if (queriedStream.me?.by?.profile?.name) {
expect(queriedStream.type).toBe("costream");
expect(queriedStream.id).toEqual(stream.id);
expect(queriedStream.core).toEqual(stream.core);
expect(queriedStream.group).toBeInstanceOf(Group);
expect(queriedStream.group.id).toBe(group.id);
expect(queriedStream.meta).toBe(null);
expect(queriedStream.perSession[node.currentSessionID].last).toEqual(
"world"
);
expect(queriedStream.perSession[node.currentSessionID].all[0].value).toEqual("hello");
expect(queriedStream.perSession[node.currentSessionID].all[0].at).toEqual(new Date(
stream.items[
node.currentSessionID
][0].madeAt
));
expect(queriedStream.perSession[node.currentSessionID].all[1].value).toEqual("world");
expect(queriedStream.perSession[node.currentSessionID].all[1].at).toEqual(new Date(
stream.items[
node.currentSessionID
][1].madeAt
));
expect(queriedStream.perSession[node.currentSessionID].by?.id).toEqual(accountID);
expect(queriedStream.perSession[node.currentSessionID].by?.profile?.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(queriedStream.perSession[node.currentSessionID].by?.profile?.name).toEqual(
"Hermes Puggington"
);
expect(queriedStream.perSession[node.currentSessionID].by?.isMe).toBe(true);
expect(queriedStream.perSession[node.currentSessionID].at).toBeInstanceOf(Date);
expect(queriedStream.perAccount[accountID].last).toEqual(
"world"
);
expect(queriedStream.perAccount[accountID].all[0].value).toEqual("hello");
expect(queriedStream.perAccount[accountID].all[0].at).toEqual(new Date(
stream.items[
node.currentSessionID
][0].madeAt
));
expect(queriedStream.perAccount[accountID].all[1].value).toEqual("world");
expect(queriedStream.perAccount[accountID].all[1].at).toEqual(new Date(
stream.items[
node.currentSessionID
][1].madeAt
));
expect(queriedStream.perAccount[accountID].by?.id).toEqual(accountID);
expect(queriedStream.perAccount[accountID].by?.profile?.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(queriedStream.perAccount[accountID].by?.profile?.name).toEqual(
"Hermes Puggington"
);
expect(queriedStream.perAccount[accountID].by?.isMe).toBe(true);
expect(queriedStream.perAccount[accountID].at).toBeInstanceOf(Date);
expect(queriedStream.me).toEqual(queriedStream.perAccount[accountID]);
// console.log("final update", queriedStream);
resolve();
unsubQuery();
}
}
});
});
stream = stream.push("hello");
stream = stream.push("world");
return done;
});
test("Streams of nested maps work", () => {
const { node } = LocalNode.withNewlyCreatedAccount({name:"Hermes Puggington"});
const group = node.createGroup();
let stream =
group.createStream<CoStream<CoMap<{ hello: "world" }>["id"]>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = node.query(stream.id, (queriedStream) => {
if (queriedStream && queriedStream.me?.last) {
// console.log("update", queriedList);
expect(queriedStream.me.last).toMatchObject({
hello: "world",
id: map.id,
});
// console.log("final update", queriedList);
resolve();
unsubQuery();
}
});
});
const map = group.createMap<CoMap<{ hello: "world" }>>({
hello: "world",
});
stream = stream.push(map.id);
return done;
});

View File

@@ -0,0 +1,22 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
},
ignorePatterns: [".eslint.cjs", "**/tests/*"],
root: true,
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
},
};

171
packages/jazz-autosub/.gitignore vendored Normal file
View File

@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -0,0 +1,2 @@
coverage
node_modules

View File

@@ -0,0 +1,19 @@
Copyright 2023, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
# CoJSON
[See the top-level README](../../README.md#cojson)

View File

@@ -0,0 +1,40 @@
{
"name": "jazz-autosub",
"module": "dist/index.js",
"main": "dist/index.js",
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.4.5",
"dependencies": {
"cojson": "^0.4.5"
},
"scripts": {
"test": "jest",
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"transform": {
"\\.[jt]sx?$": [
"ts-jest",
{
"useESM": true
}
]
},
"moduleNameMapper": {
"(.+)\\.js": "$1"
},
"extensionsToTreatAsEsm": [
".ts"
],
"modulePathIgnorePatterns": [
"/node_modules/",
"/dist/"
]
}
}

View File

@@ -0,0 +1,391 @@
import { CoList, CoMap, CoStream, Group, LocalNode, cojsonReady } from "cojson";
import { autoSub } from ".";
beforeEach(async () => {
await cojsonReady;
});
test("Queries with maps work", async () => {
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let map = group.createMap<
CoMap<{
hello: "world";
subMap: CoMap<{
hello: "world" | "moon" | "sun";
id: string;
}>["id"];
}>
>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(map.id, node, (resolvedMap) => {
// console.log("update", update);
if (resolvedMap) {
expect(resolvedMap.coValueType).toBe("comap");
expect(resolvedMap.id).toEqual(map.id);
expect(resolvedMap.meta.group).toBeInstanceOf(Group);
expect(resolvedMap.meta.group.id).toBe(group.id);
expect(resolvedMap.meta.headerMeta).toBe(null);
expect(resolvedMap.hello).toBe("world");
expect(Object.keys(resolvedMap)).toEqual(["hello", "subMap"]);
if (resolvedMap.meta.edits.hello?.by?.profile?.name) {
expect(resolvedMap.meta.edits.hello.by.id).toEqual(
accountID
);
expect(resolvedMap.meta.edits.hello.by.profile.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(
resolvedMap.meta.edits.hello.by.profile.name
).toEqual("Hermes Puggington");
expect(resolvedMap.meta.edits.hello.by.isMe).toBe(true);
expect(resolvedMap.meta.edits.hello.tx).toEqual(
map.lastEditAt("hello")!.tx
);
expect(resolvedMap.meta.edits.hello.at).toEqual(
new Date(map.lastEditAt("hello")!.at)
);
if (resolvedMap.subMap) {
expect(resolvedMap.subMap.coValueType).toBe("comap");
expect(resolvedMap.subMap.id).toEqual("foreignID");
expect(resolvedMap.subMap.meta.group).toBeInstanceOf(
Group
);
expect(resolvedMap.subMap.meta.group.id).toBe(group.id);
expect(resolvedMap.subMap.meta.headerMeta).toBe(null);
if (resolvedMap.subMap.hello === "moon") {
// console.log("got to 'moon'");
resolvedMap.subMap.set("hello", "sun");
} else if (
resolvedMap.subMap.hello === "sun" &&
resolvedMap.subMap.meta.edits.hello?.by?.profile
?.name === "Hermes Puggington"
) {
// console.log("final update", resolvedMap);
resolve();
unsubQuery();
}
}
}
}
});
});
map = map.set("hello", "world");
let subMap = group.createMap<
CoMap<{
hello: "world" | "moon" | "sun";
id: string;
}>
>();
map = map.set("subMap", subMap.id);
subMap = subMap.mutate((subMap) => {
subMap.set("hello", "world");
subMap.set("id", "foreignID");
});
subMap = subMap.set("hello", "moon");
await done;
});
test("Queries with lists work", () => {
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let list = group.createList<CoList<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
if (resolvedList) {
// console.log("update", resolvedList, resolvedList.meta.edits);
expect(resolvedList.coValueType).toBe("colist");
expect(resolvedList.id).toEqual(list.id);
expect(resolvedList.meta.group).toBeInstanceOf(Group);
expect(resolvedList.meta.group.id).toBe(group.id);
expect(resolvedList.meta.headerMeta).toBe(null);
expect(resolvedList[0]).toBe("hello");
expect(resolvedList[1]).toBe("world");
expect(resolvedList[2]).toBe("moon");
if (resolvedList.meta.edits[2]?.by?.profile?.name) {
expect(resolvedList.meta.edits[2].by.id).toEqual(accountID);
expect(resolvedList.meta.edits[2].by.profile.id).toEqual(
node.expectProfileLoaded(accountID).id
);
expect(resolvedList.meta.edits[2].by.profile.name).toEqual(
"Hermes Puggington"
);
expect(resolvedList.meta.edits[2].by.isMe).toBe(true);
expect(resolvedList.meta.edits[2].at).toBeInstanceOf(Date);
if (resolvedList.length === 3) {
resolvedList.append("sun");
} else if (
resolvedList.length === 4 &&
resolvedList.meta.edits[3]?.by?.profile?.name ===
"Hermes Puggington"
) {
expect(resolvedList[3]).toBe("sun");
// console.log("final update", resolvedList);
resolve();
unsubQuery();
}
}
}
});
});
list = list.mutate((list) => {
list.append("hello");
list.append("world");
list.append("moon");
});
return done;
});
test("List of nested maps works", () => {
const { node } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let list = group.createList<CoList<CoMap<{ hello: "world" }>["id"]>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
if (resolvedList && resolvedList[0]) {
// console.log("update", resolvedList);
expect(resolvedList[0]).toMatchObject({
hello: "world",
id: list.get(0)!,
});
// console.log("final update", resolvedList);
resolve();
unsubQuery();
}
});
});
list = list.append(
group.createMap<CoMap<{ hello: "world" }>>({
hello: "world",
}).id
);
return done;
});
test("Can call .map on a quieried coList", async () => {
const { node } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let list = group.createList<CoList<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
if (resolvedList && resolvedList[0]) {
// console.log("update", resolvedList);
expect(resolvedList.map((item) => item + "!!!")).toEqual([
"hello!!!",
"world!!!",
]);
// console.log("final update", resolvedList);
resolve();
unsubQuery();
}
});
});
list = list.mutate((list) => {
list.append("hello");
list.append("world");
});
await done;
});
test("Queries with streams work", () => {
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let stream = group.createStream<CoStream<string>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(stream.id, node, (resolvedStream) => {
if (resolvedStream) {
// console.log("update", resolvedStream);
if (resolvedStream.me?.by?.profile?.name) {
expect(resolvedStream.coValueType).toBe("costream");
expect(resolvedStream.id).toEqual(stream.id);
expect(resolvedStream.meta.group).toBeInstanceOf(Group);
expect(resolvedStream.meta.group.id).toBe(group.id);
expect(resolvedStream.meta.headerMeta).toBe(null);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].last
).toEqual("world");
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].all[0].value
).toEqual("hello");
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].all[0].at
).toEqual(
new Date(stream.items[node.currentSessionID][0].madeAt)
);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].all[1].value
).toEqual("world");
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].all[1].at
).toEqual(
new Date(stream.items[node.currentSessionID][1].madeAt)
);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].by?.id
).toEqual(accountID);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].by?.profile?.id
).toEqual(node.expectProfileLoaded(accountID).id);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].by?.profile?.name
).toEqual("Hermes Puggington");
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].by?.isMe
).toBe(true);
expect(
Object.fromEntries(resolvedStream.perSession)[
node.currentSessionID
].at
).toBeInstanceOf(Date);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.last
).toEqual("world");
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.all[0].value
).toEqual("hello");
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.all[0].at
).toEqual(
new Date(stream.items[node.currentSessionID][0].madeAt)
);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.all[1].value
).toEqual("world");
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.all[1].at
).toEqual(
new Date(stream.items[node.currentSessionID][1].madeAt)
);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.by?.id
).toEqual(accountID);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.by?.profile?.id
).toEqual(node.expectProfileLoaded(accountID).id);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.by?.profile?.name
).toEqual("Hermes Puggington");
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.by?.isMe
).toBe(true);
expect(
Object.fromEntries(resolvedStream.perAccount)[accountID]
.at
).toBeInstanceOf(Date);
expect(resolvedStream.me).toEqual(
Object.fromEntries(resolvedStream.perAccount)[accountID]
);
// console.log("final update", resolvedStream);
resolve();
unsubQuery();
}
}
});
});
stream = stream.push("hello");
stream = stream.push("world");
return done;
});
test("Streams of nested maps work", () => {
const { node } = LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
const group = node.createGroup();
let stream =
group.createStream<CoStream<CoMap<{ hello: "world" }>["id"]>>();
const done = new Promise<void>((resolve) => {
const unsubQuery = autoSub(stream.id, node, (resolvedStream) => {
if (resolvedStream && resolvedStream.me?.last) {
// console.log("update", resolvedList);
expect(resolvedStream.me.last).toMatchObject({
hello: "world",
id: map.id,
});
// console.log("final update", resolvedList);
resolve();
unsubQuery();
}
});
});
const map = group.createMap<CoMap<{ hello: "world" }>>({
hello: "world",
});
stream = stream.push(map.id);
return done;
});

View File

@@ -0,0 +1,334 @@
import {
Account,
CoID,
CoList,
CoMap,
CoStream,
CoValue,
Group,
JsonValue,
LocalNode,
cojsonInternals,
} from "cojson";
import {
ResolvedCoMap,
ResolvedCoMapBase,
} from "./resolvedCoValues/resolvedCoMap.js";
import { ResolvedCoList } from "./resolvedCoValues/resolvedCoList.js";
import { ResolvedCoStream } from "./resolvedCoValues/resolvedCoStream.js";
import { ResolvedAccount } from "./resolvedCoValues/resolvedAccount.js";
import { ResolvedGroup } from "./resolvedCoValues/resolvedGroup.js";
export type Resolved<T extends CoValue> = T extends CoMap
? T extends Account
? ResolvedAccount<T>
: T extends Group
? ResolvedGroup<T>
: ResolvedCoMap<T>
: T extends CoList
? ResolvedCoList<T>
: T extends CoStream
? T["headerMeta"] extends { type: "binary" }
? never
: ResolvedCoStream<T>
:
| ResolvedAccount
| ResolvedGroup
| ResolvedCoMap<CoMap>
| ResolvedCoList<CoList>
| ResolvedCoStream<CoStream>;
export type ValueOrResolvedRef<
V extends JsonValue | CoValue | CoID<CoValue> | undefined
> = V extends CoID<infer C>
? Resolved<C> | undefined
: V extends CoValue
? Resolved<V> | undefined
: V;
export interface AutoSubExtension<T extends CoValue, O> {
id: string;
subscribe(
base: T,
autoSubContext: AutoSubContext,
onUpdate: (value: O) => void
): () => void;
}
export class AutoSubContext {
values: {
[id: CoID<CoValue>]: {
lastUpdate: CoValue | undefined;
lastLoaded: Resolved<CoValue> | undefined;
render: () => void;
unsubscribe: () => void;
};
} = {};
extensions: {
[id: `${CoID<CoValue>}_${string}`]: {
lastOutput: unknown;
unsubscribe: () => void;
};
} = {};
node: LocalNode;
onUpdate: () => void;
constructor(node: LocalNode, onUpdate: () => void) {
this.node = node;
this.onUpdate = onUpdate;
}
autoSub<T extends CoValue>(valueID: CoID<T>, alsoRender: CoID<CoValue>[]) {
let value = this.values[valueID];
if (!value) {
const render = () => {
let newLoaded;
const lastUpdate = value!.lastUpdate;
if (lastUpdate instanceof CoMap) {
if (lastUpdate instanceof Account) {
newLoaded = new ResolvedAccount(
lastUpdate,
this
) as Resolved<T>;
} else if (lastUpdate instanceof Group) {
newLoaded = new ResolvedGroup(
lastUpdate,
this
) as Resolved<T>;
} else {
newLoaded = ResolvedCoMapBase.newWithKVPairs(
lastUpdate,
this
) as Resolved<T>;
}
} else if (lastUpdate instanceof CoList) {
newLoaded = new ResolvedCoList(
lastUpdate,
this
) as Resolved<T>;
} else if (lastUpdate instanceof CoStream) {
if (lastUpdate.headerMeta?.type === "binary") {
// Querying binary string not yet implemented
} else {
newLoaded = new ResolvedCoStream(
lastUpdate,
this
) as Resolved<T>;
}
}
// console.log(
// "Rendered ",
// valueID,
// lastUpdate?.constructor.name,
// newResolved
// );
value!.lastLoaded = newLoaded;
for (const alsoRenderID of alsoRender) {
// console.log("Also rendering", alsoRenderID);
this.values[alsoRenderID]?.render();
}
};
value = {
lastLoaded: undefined,
lastUpdate: undefined,
render,
unsubscribe: this.node.subscribe(valueID, (valueUpdate) => {
value!.lastUpdate = valueUpdate;
value!.render();
this.onUpdate();
}),
};
this.values[valueID] = value;
}
return value.lastLoaded as Resolved<T> | undefined;
}
subscribeIfCoID<T extends JsonValue | undefined>(
value: T,
alsoRender: CoID<CoValue>[]
): T extends CoID<infer C> ? Resolved<C> | undefined : T {
if (typeof value === "string" && value.startsWith("co_")) {
return this.autoSub(
value as CoID<CoValue>,
alsoRender
) as T extends CoID<infer C> ? Resolved<C> | undefined : never;
} else {
return value as T extends CoID<infer C>
? Resolved<C> | undefined
: T;
}
}
valueOrResolvedRefPropertyDescriptor<T extends JsonValue | undefined>(
value: T,
alsoRender: CoID<CoValue>[]
): T extends CoID<infer C>
? { get(): Resolved<C> | undefined }
: { value: T } {
if (typeof value === "string" && value.startsWith("co_")) {
// TODO: when we track render dirty status, we can actually return the resolved value without a getter if it's up to date
return {
get: () => this.autoSub(value as CoID<CoValue>, alsoRender),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { value: value } as any;
}
}
defineResolvedRefPropertiesIn<
O extends object,
P extends {
[key: string]: {
value: JsonValue | undefined;
enumerable: boolean;
};
}
>(
obj: O,
subqueryProps: P,
alsoRender: CoID<CoValue>[]
): O & {
[Key in keyof P]: ValueOrResolvedRef<P[Key]["value"]>;
} {
for (const [key, descriptor] of Object.entries(subqueryProps)) {
Object.defineProperty(obj, key, {
...this.valueOrResolvedRefPropertyDescriptor(
descriptor.value,
alsoRender
),
enumerable: descriptor.enumerable,
});
}
return obj as O & {
[Key in keyof P]: ValueOrResolvedRef<P[Key]["value"]>;
};
}
getOrCreateExtension<T extends CoValue, O>(
valueID: CoID<T>,
extension: AutoSubExtension<T, O>
): O | undefined {
const id = `${valueID}_${extension.id}`;
let ext = this.extensions[id as keyof typeof this.extensions];
if (!ext) {
ext = {
lastOutput: undefined,
unsubscribe: extension.subscribe(
this.node
.expectCoValueLoaded(valueID)
.getCurrentContent() as T,
this,
(output) => {
ext!.lastOutput = output;
this.values[valueID]?.render();
this.onUpdate();
}
),
};
this.extensions[id as keyof typeof this.extensions] = ext;
}
return ext.lastOutput as O | undefined;
}
cleanup() {
for (const child of Object.values(this.values)) {
child.unsubscribe?.();
}
for (const extension of Object.values(this.extensions)) {
extension.unsubscribe();
}
}
}
export function autoSub<C extends CoValue>(
id: CoID<C> | undefined,
node: LocalNode,
callback: (resolved: Resolved<C> | undefined) => void
): () => void;
export function autoSub<A extends Account = Account>(
id: "me",
node: LocalNode,
callback: (resolved: ResolvedAccount<A> | undefined) => void
): () => void;
export function autoSub(
id: CoID<CoValue> | "me" | undefined,
node: LocalNode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (resolved: any | undefined) => void
): () => void;
export function autoSub(
id: CoID<CoValue> | "me" | undefined,
node: LocalNode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (resolved: any | undefined) => void
): () => void {
// console.log("querying", id);
const effectiveId =
id === "me"
? cojsonInternals.isAccountID(node.account.id)
? node.account.id
: undefined
: id;
if (!effectiveId) return () => {};
const context = new AutoSubContext(node, () => {
const rootResolved = context.values[effectiveId]?.lastLoaded;
callback(rootResolved);
});
context.autoSub(effectiveId, []);
function cleanup() {
context.cleanup();
}
return cleanup;
}
export function autoSubResolution<
A extends Account,
O extends Resolved<CoValue>
>(
id: "me",
drillDown: (root: ResolvedAccount<A>) => O | undefined,
node: LocalNode
): Promise<O>;
export function autoSubResolution<
C extends CoValue,
O extends Resolved<CoValue>
>(
id: CoID<C> | undefined,
drillDown: (root: Resolved<C>) => O | undefined,
node: LocalNode
): Promise<O>;
export function autoSubResolution(
id: CoID<CoValue> | undefined | "me",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drillDown: (root: any) => any,
node: LocalNode
): Promise<Resolved<CoValue> | undefined>;
export function autoSubResolution(
id: CoID<CoValue> | undefined | "me",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drillDown: (root: any) => any,
node: LocalNode
): Promise<Resolved<CoValue> | undefined> {
return new Promise((resolve) => {
const cleanUp = autoSub(id, node, (root) => {
if (!root) return;
const output = drillDown(root);
if (output) {
cleanUp();
resolve(output);
}
});
});
}

View File

@@ -0,0 +1,9 @@
export { ResolvedCoStream } from "./resolvedCoValues/resolvedCoStream.js";
export { ResolvedCoList } from "./resolvedCoValues/resolvedCoList.js";
export { ResolvedCoMapBase } from "./resolvedCoValues/resolvedCoMap.js";
export type { ResolvedCoMap } from "./resolvedCoValues/resolvedCoMap.js";
export { ResolvedAccount } from "./resolvedCoValues/resolvedAccount.js";
export { ResolvedGroup } from "./resolvedCoValues/resolvedGroup.js";
export type { Resolved, AutoSubExtension, AutoSubContext } from "./autoSub.js";
export { autoSub, autoSubResolution } from "./autoSub.js";

View File

@@ -1,16 +1,21 @@
import { Account } from "../coValues/account.js";
import { CoID, CoValue, ControlledAccount, InviteSecret } from "../index.js";
import { QueryContext } from "../queries.js";
import { QueriedGroup } from "./queriedGroup.js";
import {
Account,
CoID,
CoValue,
ControlledAccount,
InviteSecret,
} from "cojson";
import { AutoSubContext } from "../autoSub.js";
import { ResolvedGroup } from "./resolvedGroup.js";
export class QueriedAccount<A extends Account = Account> extends QueriedGroup<A> {
id!: CoID<A>;
export class ResolvedAccount<
A extends Account = Account
> extends ResolvedGroup<A> {
isMe!: boolean;
constructor(account: A, queryContext: QueryContext) {
super(account, queryContext);
constructor(account: A, autoSubContext: AutoSubContext) {
super(account, autoSubContext);
Object.defineProperties(this, {
id: { value: account.id, enumerable: false },
isMe: {
value: account.core.node.account.id === account.id,
enumerable: false,
@@ -22,7 +27,7 @@ export class QueriedAccount<A extends Account = Account> extends QueriedGroup<A>
if (!this.isMe)
throw new Error("Only the current user can create a group");
return (
this.group.core.node.account as ControlledAccount
this.meta.group.core.node.account as ControlledAccount
).createGroup();
}
@@ -32,7 +37,7 @@ export class QueriedAccount<A extends Account = Account> extends QueriedGroup<A>
) {
if (!this.isMe)
throw new Error("Only the current user can accept an invite");
return (this.group.core.node.account as ControlledAccount).acceptInvite(
return (this.meta.group.core.node.account as ControlledAccount).acceptInvite(
groupOrOwnedValueID,
inviteSecret
);

View File

@@ -0,0 +1,254 @@
import {
CoID,
CoList,
Group,
MutableCoList,
CojsonInternalTypes,
CoValue,
} from "cojson";
import { ValueOrResolvedRef, AutoSubContext } from "../autoSub.js";
import { ResolvedAccount } from "./resolvedAccount.js";
export type ResolvedCoListEdit<L extends CoList> = {
by?: ResolvedAccount;
tx: CojsonInternalTypes.TransactionID;
at: Date;
value: L["_item"] extends CoValue
? CoID<L["_item"]>
: Exclude<L["_item"], CoValue>;
};
export type ResolvedCoListDeletion = {
by?: ResolvedAccount;
tx: CojsonInternalTypes.TransactionID;
at: Date;
};
export type ResolvedCoListMeta<L extends CoList> = {
coValue: L;
edits: ResolvedCoListEdit<L>[];
deletions: ResolvedCoListDeletion[];
headerMeta: L["headerMeta"];
group: Group;
};
export class ResolvedCoList<L extends CoList> extends Array<
ValueOrResolvedRef<L["_item"]>
> {
id!: CoID<L>;
coValueType!: "colist";
meta!: ResolvedCoListMeta<L>;
/** @internal */
constructor(coList: L, autoSubContext: AutoSubContext) {
if (!(coList instanceof CoList)) {
// this might be called from an intrinsic, like map, trying to create an empty array
// passing `0` as the only parameter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Array(coList) as any;
}
super(
...coList
.asArray()
.map(
(item) =>
autoSubContext.subscribeIfCoID(item, [
coList.id,
]) as ValueOrResolvedRef<L["_item"]>
)
);
Object.defineProperties(this, {
id: { value: coList.id, enumerable: false },
coValueType: { value: "colist", enumerable: false },
meta: {
value: {
coValue: coList,
edits: [...this.keys()].map((i) => {
const edit = coList.editAt(i)!;
return autoSubContext.defineResolvedRefPropertiesIn(
{
tx: edit.tx,
at: new Date(edit.at),
},
{
by: { value: edit.by, enumerable: true },
value: { value: edit.value, enumerable: true },
},
[coList.id]
) as ResolvedCoListEdit<L>;
}),
deletions: coList.deletionEdits().map(
(deletion) =>
autoSubContext.defineResolvedRefPropertiesIn(
{
tx: deletion.tx,
at: new Date(deletion.at),
},
{
by: {
value: deletion.by,
enumerable: true,
},
},
[coList.id]
) as ResolvedCoListDeletion
),
headerMeta: coList.headerMeta,
group: coList.group,
} satisfies ResolvedCoListMeta<L>,
enumerable: false,
},
});
}
append(
item: L["_item"],
after?: number,
privacy?: "private" | "trusting"
): L {
return this.meta.coValue.append(item, after, privacy);
}
prepend(
item: L["_item"],
before?: number,
privacy?: "private" | "trusting"
): L {
return this.meta.coValue.prepend(item, before, privacy);
}
delete(at: number, privacy?: "private" | "trusting"): L {
return this.meta.coValue.delete(at, privacy);
}
mutate(
mutator: (mutable: MutableCoList<L["_item"], L["headerMeta"]>) => void
): L {
return this.meta.coValue.mutate(mutator);
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isArray(arg: any): arg is any[] {
return Array.isArray(arg);
}
/** @internal */
static from<T>(arrayLike: ArrayLike<T>): T[];
/** @internal */
static from<T, U>(
arrayLike: ArrayLike<T>,
mapfn: (v: T, k: number) => U,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
thisArg?: any
): U[];
/** @internal */
static from<T>(iterable: Iterable<T> | ArrayLike<T>): T[];
/** @internal */
static from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapfn: (v: T, k: number) => U,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
thisArg?: any
): U[];
/** @internal */
static from<T, U>(
_iterable: unknown,
_mapfn?: unknown,
_thisArg?: unknown
): T[] | U[] | T[] | U[] {
throw new Error("Array method 'from' not supported on ResolvedCoList");
}
/** @internal */
static of<T>(..._items: T[]): T[] {
throw new Error("Array method 'of' not supported on ResolvedCoList");
}
/** @internal */
pop(): ValueOrResolvedRef<L["_item"]> | undefined {
throw new Error("Array method 'pop' not supported on ResolvedCoList");
}
/** @internal */
push(..._items: ValueOrResolvedRef<L["_item"]>[]): number {
throw new Error("Array method 'push' not supported on ResolvedCoList");
}
/** @internal */
concat(
..._items: ConcatArray<ValueOrResolvedRef<L["_item"]>>[]
): ValueOrResolvedRef<L["_item"]>[];
/** @internal */
concat(
..._items: (
| ValueOrResolvedRef<L["_item"]>
| ConcatArray<ValueOrResolvedRef<L["_item"]>>
)[]
): ValueOrResolvedRef<L["_item"]>[];
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
concat(..._items: any[]): ValueOrResolvedRef<L["_item"]>[] {
throw new Error("Array method 'concat' not supported on ResolvedCoList");
}
/** @internal */
reverse(): ValueOrResolvedRef<L["_item"]>[] {
throw new Error(
"Array method 'reverse' not supported on ResolvedCoList"
);
}
/** @internal */
shift(): ValueOrResolvedRef<L["_item"]> | undefined {
throw new Error("Array method 'shift' not supported on ResolvedCoList");
}
/** @internal */
sort(
_compareFn?:
| ((
a: ValueOrResolvedRef<L["_item"]>,
b: ValueOrResolvedRef<L["_item"]>
) => number)
| undefined
): this {
throw new Error("Array method 'sort' not supported on ResolvedCoList");
}
/** @internal */
splice(
_start: number,
_deleteCount?: number | undefined
): ValueOrResolvedRef<L["_item"]>[] {
throw new Error("Array method 'splice' not supported on ResolvedCoList");
}
/** @internal */
unshift(..._items: ValueOrResolvedRef<L["_item"]>[]): number {
throw new Error(
"Array method 'unshift' not supported on ResolvedCoList"
);
}
/** @internal */
fill(
_value: ValueOrResolvedRef<L["_item"]>,
_start?: number | undefined,
_end?: number | undefined
): this {
throw new Error("Array method 'fill' not supported on ResolvedCoList");
}
/** @internal */
copyWithin(
_target: number,
_start: number,
_end?: number | undefined
): this {
throw new Error(
"Array method 'copyWithin' not supported on ResolvedCoList"
);
}
}

View File

@@ -0,0 +1,171 @@
import { CoID, CoMap, Group, MutableCoMap, CojsonInternalTypes } from "cojson";
import { ValueOrResolvedRef, AutoSubContext, AutoSubExtension } from "../autoSub.js";
import { ResolvedAccount } from "./resolvedAccount.js";
export type ResolvedCoMap<M extends CoMap> = {
[K in keyof M["_shape"] & string]: ValueOrResolvedRef<M["_shape"][K]>;
} & ResolvedCoMapBase<M>;
export type ResolvedCoMapEdit<M extends CoMap, K extends keyof M["_shape"]> = {
by?: ResolvedAccount;
tx: CojsonInternalTypes.TransactionID;
at: Date;
value: M["_shape"][K];
};
export type ResolvedCoMapLastAndAllEdits<
M extends CoMap,
K extends keyof M["_shape"]
> = ResolvedCoMapEdit<M, K> & {
all: ResolvedCoMapEdit<M, K>[];
};
export type ResolvedCoMapMeta<M extends CoMap> = {
coValue: M;
edits: {
[K in keyof M["_shape"] & string]:
| ResolvedCoMapLastAndAllEdits<M, K>
| undefined;
};
headerMeta: M["headerMeta"];
group: Group;
};
export class ResolvedCoMapBase<M extends CoMap> {
id!: CoID<M>;
coValueType!: "comap";
meta!: ResolvedCoMapMeta<M>;
/** @internal */
static newWithKVPairs<M extends CoMap>(
coMap: M,
autoSubContext: AutoSubContext
): ResolvedCoMap<M> {
const kv = {} as {
[K in keyof M["_shape"] & string]: ValueOrResolvedRef<M["_shape"][K]>;
};
for (const key of coMap.keys()) {
const value = coMap.get(key);
if (value === undefined) continue;
autoSubContext.defineResolvedRefPropertiesIn(
kv,
{
[key]: { value, enumerable: true },
},
[coMap.id]
);
}
return Object.assign(new ResolvedCoMapBase(coMap, autoSubContext), kv);
}
/** @internal */
constructor(coMap: M, autoSubContext: AutoSubContext) {
Object.defineProperties(this, {
id: { value: coMap.id, enumerable: false },
coValueType: { value: "comap", enumerable: false },
meta: {
value: {
coValue: coMap,
edits: Object.fromEntries(
coMap.keys().flatMap((key) => {
const edits = [...coMap.editsAt(key)].map(
(edit) =>
autoSubContext.defineResolvedRefPropertiesIn(
{
tx: edit.tx,
at: new Date(edit.at),
},
{
by: {
value: edit.by,
enumerable: true,
},
value: {
value: edit.value,
enumerable: true,
},
},
[coMap.id]
) as ResolvedCoMapEdit<M, keyof M["_shape"]>
);
const lastEdit = edits[edits.length - 1];
if (!lastEdit) return [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const editsAtKey = {
by: lastEdit.by,
tx: lastEdit.tx,
at: lastEdit.at,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: lastEdit.value as any,
all: edits,
};
return [[key, editsAtKey]];
})
) as {
[K in keyof M["_shape"] & string]:
| ResolvedCoMapLastAndAllEdits<M, K>
| undefined;
},
headerMeta: coMap.headerMeta,
group: coMap.group,
} satisfies ResolvedCoMapMeta<M>,
enumerable: false,
},
as: {
value: <O>(extension: AutoSubExtension<M, O>) => {
return autoSubContext.getOrCreateExtension(
coMap.id,
extension
);
},
enumerable: false,
},
});
}
get<K extends keyof M["_shape"] & string>(key: K): ResolvedCoMap<M>[K] {
return (this as ResolvedCoMap<M>)[key];
}
set<K extends keyof M["_shape"] & string>(
key: K,
value: M["_shape"][K],
privacy?: "private" | "trusting"
): M;
set(
kv: {
[K in keyof M["_shape"] & string]?: M["_shape"][K];
},
privacy?: "private" | "trusting"
): M;
set<K extends keyof M["_shape"] & string>(
...args:
| [
{
[K in keyof M["_shape"] & string]?: M["_shape"][K];
},
("private" | "trusting")?
]
| [K, M["_shape"][K], ("private" | "trusting")?]
): M {
// eslint-disable-next-line @typescript-eslint/ban-types
return (this.meta.coValue.set as Function)(...args);
}
delete(
key: keyof M["_shape"] & string,
privacy?: "private" | "trusting"
): M {
return this.meta.coValue.delete(key, privacy);
}
mutate(
mutator: (mutable: MutableCoMap<M["_shape"], M["headerMeta"]>) => void
): M {
return this.meta.coValue.mutate(mutator);
}
as!: <O>(extension: AutoSubExtension<M, O>) => O | undefined;
}

View File

@@ -0,0 +1,146 @@
import {
CoValue,
JsonValue,
CojsonInternalTypes,
CoStream,
CoID,
cojsonInternals,
Group,
AccountID,
SessionID,
MutableCoStream,
} from "cojson";
import { ValueOrResolvedRef, AutoSubContext } from "../autoSub.js";
import { ResolvedAccount } from "./resolvedAccount.js";
export type ResolvedCoStreamEntry<Item extends JsonValue | CoValue> = {
last?: ValueOrResolvedRef<Item>;
by?: ResolvedAccount;
tx?: CojsonInternalTypes.TransactionID;
at?: Date;
all: {
value: ValueOrResolvedRef<Item>;
by?: ResolvedAccount;
tx: CojsonInternalTypes.TransactionID;
at: Date;
}[];
};
export type ResolvedCoStreamMeta<S extends CoStream> = {
coValue: S;
headerMeta: S["headerMeta"];
group: Group;
}
export class ResolvedCoStream<S extends CoStream> {
id: CoID<S>;
coValueType = "costream" as const;
meta: ResolvedCoStreamMeta<S>;
me?: ResolvedCoStreamEntry<S["_item"]>;
perAccount: [account: AccountID, items: ResolvedCoStreamEntry<S["_item"]>][];
perSession: [session: SessionID, items: ResolvedCoStreamEntry<S["_item"]>][];
/** @internal */
constructor(coStream: S, autoSubContext: AutoSubContext) {
this.id = coStream.id;
this.meta = {
coValue: coStream,
headerMeta: coStream.headerMeta,
group: coStream.group,
}
this.perSession = coStream.sessions().map((sessionID) => {
const items = [...coStream.itemsIn(sessionID)].map((item) =>
autoSubContext.defineResolvedRefPropertiesIn(
{
tx: item.tx,
at: new Date(item.at),
},
{
by: {
value: cojsonInternals.isAccountID(item.by)
? item.by
: (undefined as never),
enumerable: true,
},
value: {
value: item.value as S["_item"],
enumerable: true,
},
},
[coStream.id]
)
);
const lastItem = items[items.length - 1];
return [
sessionID,
{
get last() {
return lastItem?.value;
},
get by() {
return lastItem?.by;
},
tx: lastItem?.tx,
at: lastItem?.at,
all: items,
} satisfies ResolvedCoStreamEntry<S["_item"]>,
];
});
this.perAccount = [...coStream.accounts()].map((accountID) => {
const items = [...coStream.itemsBy(accountID)].map((item) =>
autoSubContext.defineResolvedRefPropertiesIn(
{
tx: item.tx,
at: new Date(item.at),
},
{
by: {
value: cojsonInternals.isAccountID(item.by)
? item.by
: (undefined as never),
enumerable: true,
},
value: {
value: item.value as S["_item"],
enumerable: true,
},
},
[coStream.id]
)
);
const lastItem = items[items.length - 1];
const entry = {
get last() {
return lastItem?.value;
},
get by() {
return lastItem?.by;
},
tx: lastItem?.tx,
at: lastItem?.at,
all: items,
} satisfies ResolvedCoStreamEntry<S["_item"]>;
if (accountID === autoSubContext.node.account.id) {
this.me = entry;
}
return [accountID, entry];
});
}
push(item: S["_item"], privacy?: "private" | "trusting"): S {
return this.meta.coValue.push(item, privacy);
}
mutate(
mutator: (mutable: MutableCoStream<S["_item"], S["headerMeta"]>) => void
): S {
return this.meta.coValue.mutate(mutator);
}
}

View File

@@ -0,0 +1,97 @@
import {
AccountID,
BinaryCoStream,
CoID,
CoList,
CoMap,
CoStream,
Everyone,
Group,
InviteSecret,
Role,
} from "cojson";
import { AutoSubContext, ValueOrResolvedRef } from "../autoSub.js";
export class ResolvedGroupMeta<G extends Group> {
coValue!: G;
group!: G;
headerMeta!: G["headerMeta"];
}
export class ResolvedGroup<G extends Group = Group> {
id!: CoID<G>;
coValueType = "group" as const;
profile?: ValueOrResolvedRef<G["_shape"]["profile"]>;
root?: ValueOrResolvedRef<G["_shape"]["root"]>;
meta!: ResolvedGroupMeta<G>;
constructor(group: G, autoSubContext: AutoSubContext) {
const profileID = group.get("profile");
const rootID = group.get("root");
autoSubContext.defineResolvedRefPropertiesIn(
Object.defineProperties(this, {
id: { value: group.id, enumerable: false },
coValueType: { value: "group", enumerable: false },
meta: {
value: {
coValue: group,
group,
headerMeta: group.headerMeta,
},
enumerable: false,
},
}),
{
profile: {
value: profileID,
enumerable: false,
},
root: {
value: rootID,
enumerable: false,
},
},
[group.id]
);
}
addMember(accountID: AccountID | Everyone, role: Role): G {
return this.meta.group.addMember(accountID, role);
}
removeMember(accountID: AccountID): G {
return this.meta.group.removeMember(accountID);
}
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {
return this.meta.group.createInvite(role);
}
createMap<M extends CoMap>(
init?: {
[K in keyof M["_shape"]]: M["_shape"][K];
},
meta?: M["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): M {
return this.meta.group.createMap(init, meta, initPrivacy);
}
createList<L extends CoList>(
init?: L["_item"][],
meta?: L["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): L {
return this.meta.group.createList(init, meta, initPrivacy);
}
createStream<C extends CoStream>(meta?: C["headerMeta"]): C {
return this.meta.group.createStream(meta);
}
createBinaryStream<C extends BinaryCoStream>(
meta: C["headerMeta"] = { type: "binary" }
): C {
return this.meta.group.createBinaryStream(meta);
}
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "ES2020",
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.test.*"],
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,16 @@
{
"name": "jazz-browser-auth-local",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"jazz-browser": "^0.4.0",
"jazz-browser": "^0.4.5",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"

View File

@@ -1,18 +1,19 @@
{
"name": "jazz-browser-media-images",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.4.0",
"cojson": "^0.4.5",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "^0.4.0",
"jazz-autosub": "^0.4.5",
"jazz-browser": "^0.4.5",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"devDependencies": {

View File

@@ -1,18 +1,20 @@
import { CoID, Group, LocalNode, Media, QueryExtension } from "cojson";
import { CoID, Group, LocalNode, Media } from "cojson";
import ImageBlobReduce from "image-blob-reduce";
import Pica from "pica";
import {
AutoSubContext,
AutoSubExtension,
createBinaryStreamFromBlob,
readBlobFromBinaryStream,
ResolvedGroup
} from "jazz-browser";
import { QueryContext } from "cojson/dist/queries";
const pica = new Pica();
export async function createImage(
imageBlobOrFile: Blob | File,
inGroup: Group,
inGroup: Group | ResolvedGroup,
maxSize?: 256 | 1024 | 2048
): Promise<Media.ImageDefinition> {
let originalWidth!: number;
@@ -132,6 +134,21 @@ export async function createImage(
return imageDefinition;
}
export const BrowserImage: AutoSubExtension<
Media.ImageDefinition,
LoadingImageInfo
> = {
id: "BrowserImage",
subscribe(
imageDef: Media.ImageDefinition,
autoSubContext: AutoSubContext,
callback: (update: LoadingImageInfo) => void
): () => void {
return loadImage(imageDef, autoSubContext.node, callback);
},
};
export type LoadingImageInfo = {
originalSize?: [number, number];
placeholderDataURL?: string;
@@ -299,7 +316,8 @@ export function loadImage(
originalSize,
placeholderDataURL,
highestResSrc: blobURL,
highestResSrcOrPlaceholder: blobURL
highestResSrcOrPlaceholder:
blobURL,
});
unsubFromStream();
@@ -348,18 +366,3 @@ export function loadImage(
return cleanUp;
}
export const BrowserImage: QueryExtension<
Media.ImageDefinition,
LoadingImageInfo
> = {
id: "BrowserImage",
query(
imageDef: Media.ImageDefinition,
queryContext: QueryContext,
callback: (update: LoadingImageInfo) => void
): () => void {
return loadImage(imageDef, queryContext.node, callback);
},
};

View File

@@ -1,17 +1,18 @@
{
"name": "jazz-browser",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.4.0",
"cojson-storage-indexeddb": "^0.4.0",
"cojson": "^0.4.5",
"cojson-storage-indexeddb": "^0.4.5",
"jazz-autosub": "^0.4.5",
"typescript": "^5.1.6"
},
"scripts": {
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"

View File

@@ -2,7 +2,6 @@ import {
AccountMigration,
BinaryCoStream,
CoValue,
CoValueCore,
InviteSecret,
} from "cojson";
import { BinaryCoStreamMeta } from "cojson";
@@ -21,6 +20,9 @@ import {
} from "cojson";
import { ReadableStream, WritableStream } from "isomorphic-streams";
import { IDBStorage } from "cojson-storage-indexeddb";
import { Resolved } from "jazz-autosub";
export * from "jazz-autosub";
export type BrowserNodeHandle = {
node: LocalNode;
@@ -329,8 +331,8 @@ function websocketWritableStream<T>(ws: WebSocket) {
}
}
export function createInviteLink(
value: CoValue | { id: CoID<CoValue>; core: CoValueCore },
export function createInviteLink<C extends CoValue>(
value: C | Resolved<C>,
role: "reader" | "writer" | "admin",
// default to same address as window.location, but without hash
{
@@ -338,7 +340,8 @@ export function createInviteLink(
valueHint,
}: { baseURL?: string; valueHint?: string } = {}
): string {
const coValueCore = value.core;
const coValueCore =
"coValueType" in value ? value.meta.coValue.core : value.core;
let currentCoValue = coValueCore;
while (currentCoValue.header.ruleset.type === "ownedByGroup") {
@@ -425,8 +428,8 @@ export async function createBinaryStreamFromBlob<
C extends BinaryCoStream<BinaryCoStreamMeta>
>(
blob: Blob | File,
inGroup: Group,
meta: C["meta"] = { type: "binary" }
inGroup: Group | Resolved<Group>,
meta: C["headerMeta"] = { type: "binary" }
): Promise<C> {
let stream = inGroup.createBinaryStream(meta);

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react-auth-local",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.tsx",
"license": "MIT",
"dependencies": {
"jazz-browser-auth-local": "^0.4.0",
"jazz-react": "^0.4.0",
"jazz-browser-auth-local": "^0.4.5",
"jazz-react": "^0.4.5",
"typescript": "^5.1.6"
},
"devDependencies": {
@@ -17,7 +17,7 @@
},
"scripts": {
"lint": "eslint src/**/*.tsx",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"

View File

@@ -1,4 +1,3 @@
import React from "react";
import { useMemo, useState, ReactNode } from "react";
import { BrowserLocalAuth } from "jazz-browser-auth-local";
import { ReactAuthHook } from "jazz-react";

View File

@@ -6,7 +6,7 @@
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"jsx": "react",
"jsx": "react-jsx",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react",
"version": "0.4.0",
"version": "0.4.5",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.4.0",
"jazz-browser": "^0.4.0",
"cojson": "^0.4.5",
"jazz-browser": "^0.4.5",
"typescript": "^5.1.6"
},
"devDependencies": {
@@ -17,7 +17,7 @@
},
"scripts": {
"lint": "eslint src/**/*.tsx",
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
},
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"

View File

@@ -1,27 +1,35 @@
import {
LocalNode,
CoID,
Queried,
CoValue,
BinaryCoStream,
QueriedAccount,
Account,
AccountMeta,
AccountMigration,
Profile,
CoMap,
} from "cojson";
import React, { useEffect, useState } from "react";
import {
AuthProvider,
consumeInviteLinkFromWindowLocation,
createBrowserNode,
readBlobFromBinaryStream,
} from "jazz-browser";
import { readBlobFromBinaryStream } from "jazz-browser";
import { Profile } from "cojson";
import { CoMap } from "cojson";
import { Resolved, ResolvedAccount, autoSub } from "jazz-autosub";
export type { Resolved, ResolvedCoMap } from "jazz-autosub";
export {
ResolvedAccount,
ResolvedCoList,
ResolvedCoMapBase,
ResolvedCoStream,
ResolvedGroup,
} from "jazz-autosub";
const JazzContext = React.createContext<
| {
me: Queried<Account>;
me: Resolved<Account>;
localNode: LocalNode;
logOut: () => void;
}
@@ -100,7 +108,7 @@ export function WithJazz(props: {
};
}, [auth, syncAddress]);
const me = useSyncedQueryWithNode("me", node) as QueriedAccount | undefined;
const me = useAutoSubWithNode("me", node) as ResolvedAccount | undefined;
return (
<>
@@ -122,8 +130,9 @@ export function WithJazz(props: {
}
/**
* Hook that exposes the Jazz context provided by `<WithJazz/>`, most importantly the `LocalNode`
* for the logged in user (which you can use to create `Group`s, and `CoValue`s in those).
* Hook that exposes the Jazz context provided by `<WithJazz/>`, most importantly `me`, the account of
* the current in user (which you can use access the account's `root` or `profile`,
* and to create `Group`s as the current user, in which you can then create `CoValue`s).
*
* Also provides a `logOut` function, which invokes the log-out logic of the Auth Provider passed to `<WithJazz/>`.
*/
@@ -139,7 +148,7 @@ export function useJazz<
}
return {
me: context.me as QueriedAccount<Account<P, R, Meta>>,
me: context.me as ResolvedAccount<Account<P, R, Meta>>,
localNode: context.localNode,
logOut: context.logOut,
};
@@ -148,36 +157,44 @@ export function useJazz<
/**
* Hook that subscribes to all updates of a given `CoValue` (identified by its `CoID`) and that automatically resolves references to nested `CoValue`s, loading and subscribing to them as well.
*
* See `Queried<T>` in `cojson` to see which fields and methods are available on the returned object.
* See `Resolved<T>` in `jazz-autosub` to see which fields and methods are available on the returned object.
*
* @param id The `CoID` of the `CoValue` to subscribe to. Can be undefined (in which case the hook returns undefined).
*/
export function useSyncedQuery<
export function useAutoSub<T extends CoValue>(
id?: CoID<T>
): Resolved<T> | undefined;
/**
* Hook that subscribes to all updates the current user account and that automatically resolves references to nested `CoValue`s, loading and subscribing to them as well.
*
* See `Resolved<T>` in `jazz-autosub` to see which fields and methods are available on the returned object.
*
*/
export function useAutoSub<
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
>(id: "me"): QueriedAccount<Account<P, R, Meta>> | undefined;
export function useSyncedQuery<T extends CoValue>(
id?: CoID<T>
): Queried<T> | undefined;
export function useSyncedQuery(
>(id: "me"): ResolvedAccount<Account<P, R, Meta>> | undefined;
export function useAutoSub(
id?: CoID<CoValue> | "me"
): Queried<CoValue> | QueriedAccount | undefined {
return useSyncedQueryWithNode(id, useJazz().localNode);
): Resolved<CoValue> | ResolvedAccount | undefined {
return useAutoSubWithNode(id, useJazz().localNode);
}
/** @internal */
export function useSyncedQueryWithNode(
function useAutoSubWithNode(
id?: CoID<CoValue> | "me",
localNode?: LocalNode
): Queried<CoValue> | QueriedAccount | undefined {
): Resolved<CoValue> | ResolvedAccount | undefined {
const [result, setResult] = useState<
Queried<CoValue> | QueriedAccount | undefined
Resolved<CoValue> | ResolvedAccount | undefined
>();
useEffect(() => {
if (!id || !localNode) return;
const unsubscribe = localNode.query(id, setResult);
const unsubscribe = autoSub(id, localNode, setResult);
return unsubscribe;
}, [id, localNode]);

208
yarn.lock
View File

@@ -473,6 +473,33 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d"
integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==
"@floating-ui/core@^1.4.2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
dependencies:
"@floating-ui/utils" "^0.1.3"
"@floating-ui/dom@^1.5.1":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
dependencies:
"@floating-ui/core" "^1.4.2"
"@floating-ui/utils" "^0.1.3"
"@floating-ui/react-dom@^2.0.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20"
integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==
dependencies:
"@floating-ui/dom" "^1.5.1"
"@floating-ui/utils@^0.1.3":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.4.tgz#19654d1026cc410975d46445180e70a5089b3e7d"
integrity sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==
"@humanwhocodes/config-array@^0.11.10":
version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
@@ -1175,6 +1202,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-arrow@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
@@ -1227,6 +1262,82 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/react-dismissable-layer@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-scope@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-id@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popper@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-use-rect" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-portal@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
@@ -1235,6 +1346,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-portal@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
@@ -1316,6 +1435,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-use-size@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
@@ -1332,6 +1459,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@remix-run/router@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
@@ -2159,6 +2293,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
dependencies:
tslib "^2.0.0"
aria-query@^5.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
@@ -3152,6 +3293,11 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
devtools-protocol@0.0.1147663:
version "0.0.1147663"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz#4ec5610b39a6250d1f87e6b9c7e16688ed0ac78e"
@@ -4022,6 +4168,11 @@ get-func-name@^2.0.0:
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -4561,6 +4712,13 @@ inquirer@^8.2.4:
through "^2.3.6"
wrap-ansi "^6.0.1"
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
ip@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
@@ -5548,7 +5706,7 @@ loglevel@^1.6.0:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -7007,6 +7165,25 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-remove-scroll-bar@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9"
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
dependencies:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-router-dom@^6.16.0:
version "6.16.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.16.0.tgz#86f24658da35eb66727e75ecbb1a029e33ee39d9"
@@ -7022,6 +7199,15 @@ react-router@6.16.0, react-router@^6.16.0:
dependencies:
"@remix-run/router" "1.9.0"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^2.0.0"
react-time-ago@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/react-time-ago/-/react-time-ago-7.2.1.tgz#101a549a8d7f51c225c82c74abc517ecef647b24"
@@ -8021,6 +8207,11 @@ tsconfig-paths@^4.1.2:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
@@ -8207,6 +8398,21 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-callback-ref@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==
dependencies:
tslib "^2.0.0"
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
dependencies:
detect-node-es "^1.1.0"
tslib "^2.0.0"
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"