Compare commits
11 Commits
jazz-brows
...
jazz-brows
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccebd2447d | ||
|
|
08dca75789 | ||
|
|
f1cd639a09 | ||
|
|
be18e4de14 | ||
|
|
7e62c91d44 | ||
|
|
b2d5a103b5 | ||
|
|
4ee2cad39e | ||
|
|
b7c8a0038b | ||
|
|
8c27e8c379 | ||
|
|
0133aa47ff | ||
|
|
5659c925a2 |
20
README.md
20
README.md
@@ -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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.19",
|
||||
"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.1",
|
||||
"jazz-react": "^0.4.1",
|
||||
"jazz-react-auth-local": "^0.4.1",
|
||||
"jazz-browser-media-images": "^0.4.7",
|
||||
"jazz-react": "^0.4.6",
|
||||
"jazz-react-auth-local": "^0.4.6",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,7 +73,7 @@ export function RatePetPostUI() {
|
||||
function ReactionOverview({
|
||||
petReactions,
|
||||
}: {
|
||||
petReactions: Queried<PetReactions>;
|
||||
petReactions: Resolved<PetReactions>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.41",
|
||||
"version": "0.0.43",
|
||||
"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.1",
|
||||
"jazz-react-auth-local": "^0.4.1",
|
||||
"jazz-react": "^0.4.6",
|
||||
"jazz-react-auth-local": "^0.4.6",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider, createHashRouter } from "react-router-dom";
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
import { WithJazz, useJazz, useAcceptInvite } from "jazz-react";
|
||||
@@ -14,8 +18,8 @@ import {
|
||||
import { PrettyAuthUI } from "./components/Auth.tsx";
|
||||
import { NewProjectForm } from "./3_NewProjectForm.tsx";
|
||||
import { ProjectTodoTable } from "./4_ProjectTodoTable.tsx";
|
||||
import { migration } from "./1_types.ts";
|
||||
import { AccountMigration } from "cojson";
|
||||
import { TodoAccountRoot, migration } from "./1_types.ts";
|
||||
import { AccountMigration, Profile } from "cojson";
|
||||
|
||||
/**
|
||||
* Walkthrough: The top-level provider `<WithJazz/>`
|
||||
@@ -61,7 +65,7 @@ function App() {
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <NewProjectForm />,
|
||||
element: <HomeScreen />,
|
||||
},
|
||||
{
|
||||
path: "/project/:projectId",
|
||||
@@ -91,4 +95,27 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeScreen() {
|
||||
const { me } = useJazz<Profile, TodoAccountRoot>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
{me.root?.projects?.length ? <h1>My Projects</h1> : null}
|
||||
{me.root?.projects?.map((project) => {
|
||||
return (
|
||||
<Button
|
||||
key={project?.id}
|
||||
onClick={() => navigate("/project/" + project?.id)}
|
||||
variant="ghost"
|
||||
>
|
||||
{project?.title}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<NewProjectForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Walkthrough: Continue with ./3_NewProjectForm.tsx */
|
||||
|
||||
@@ -2,17 +2,17 @@ import { useCallback } from "react";
|
||||
|
||||
import { useJazz } from "jazz-react";
|
||||
|
||||
import { ListOfTasks, TodoProject } from "./1_types";
|
||||
import { ListOfTasks, TodoAccountRoot, TodoProject } from "./1_types";
|
||||
|
||||
import { SubmittableInput } from "./basicComponents";
|
||||
|
||||
import { useNavigate } from "react-router";
|
||||
import { Profile } from "cojson";
|
||||
|
||||
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<Profile, TodoAccountRoot>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createProject = useCallback(
|
||||
@@ -22,7 +22,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>({
|
||||
@@ -30,9 +30,11 @@ export function NewProjectForm() {
|
||||
tasks: projectGroup.createList<ListOfTasks>().id,
|
||||
});
|
||||
|
||||
me.root?.projects?.append(project.id);
|
||||
|
||||
navigate("/project/" + project.id);
|
||||
},
|
||||
[localNode, navigate]
|
||||
[me, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-twit",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -18,9 +18,9 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"javascript-time-ago": "^2.5.9",
|
||||
"jazz-browser-media-images": "^0.4.1",
|
||||
"jazz-react": "^0.4.1",
|
||||
"jazz-react-auth-local": "^0.4.1",
|
||||
"jazz-browser-media-images": "^0.4.7",
|
||||
"jazz-react": "^0.4.6",
|
||||
"jazz-react-auth-local": "^0.4.6",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ export function ChronoFeed() {
|
||||
() =>
|
||||
[...(myTwits || []), ...twitsFromFollows]
|
||||
.flatMap(tw => (tw ? (tw.isReplyTo ? [] : tw) : []))
|
||||
.sort((a, b) => (b.edits.text?.at?.getTime() || 0) - (a.edits.text?.at?.getTime() || 0)),
|
||||
.sort((a, b) => (b.meta.edits.text?.at?.getTime() || 0) - (a.meta.edits.text?.at?.getTime() || 0)),
|
||||
[myTwits, twitsFromFollows]
|
||||
);
|
||||
|
||||
|
||||
@@ -15,21 +15,21 @@ import {
|
||||
TwitText,
|
||||
} from './basicComponents/index.tsx';
|
||||
import { Twit, TwitProfile } from './1_dataModel.ts';
|
||||
import { Queried } from 'cojson';
|
||||
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?: Queried<Twit>;
|
||||
twit?: Resolved<Twit>;
|
||||
alreadyInReplies?: boolean;
|
||||
}) {
|
||||
const [showReplyForm, setShowReplyForm] = React.useState(false);
|
||||
|
||||
const posterProfile = twit?.edits.text?.by?.profile as Queried<TwitProfile> | undefined;
|
||||
const posterProfile = twit?.meta.edits.text?.by?.profile as Resolved<TwitProfile> | undefined;
|
||||
const isTopLevel = !twit?.isReplyTo || alreadyInReplies;
|
||||
|
||||
return (
|
||||
@@ -47,7 +47,7 @@ export function TwitComponent({
|
||||
<Link to={'/' + posterProfile?.id} className="font-bold hover:underline">
|
||||
{posterProfile?.name}
|
||||
</Link>
|
||||
<SubtleRelativeTimeAgo dateTime={twit?.edits.text?.at} />
|
||||
<SubtleRelativeTimeAgo dateTime={twit?.meta.edits.text?.at} />
|
||||
</TwitHeader>
|
||||
|
||||
<TwitText style={posterProfile?.twitStyle}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useJazz, useSyncedQuery } from 'jazz-react';
|
||||
import { useJazz, useAutoSub } from 'jazz-react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
BioInput,
|
||||
@@ -24,7 +24,7 @@ export function ProfilePage() {
|
||||
const { profileId } = useParams<{ profileId: CoID<TwitProfile> }>();
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const profile = useSyncedQuery(profileId);
|
||||
const profile = useAutoSub(profileId);
|
||||
const isMe = profile?.id == me.profile?.id;
|
||||
|
||||
const profileTwitsAndRepliedToTwits = useMemo(() => {
|
||||
@@ -65,7 +65,7 @@ export function ProfilePage() {
|
||||
<ChooseProfilePicInput
|
||||
onChange={(file: File) =>
|
||||
me.root?.peopleWhoCanSeeMyTwits &&
|
||||
createImage(file, me.root.peopleWhoCanSeeMyTwits.group, 256).then(image => {
|
||||
createImage(file, me.root.peopleWhoCanSeeMyTwits, 256).then(image => {
|
||||
me.profile?.set({ avatar: image.id }, 'trusting');
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useJazz } from 'jazz-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 { Queried } from 'cojson';
|
||||
import { createImage } from 'jazz-browser-media-images';
|
||||
|
||||
export function CreateTwitForm(
|
||||
props: {
|
||||
inReplyTo?: Queried<Twit>;
|
||||
inReplyTo?: Resolved<Twit>;
|
||||
onSubmit?: () => void;
|
||||
className?: string;
|
||||
} = {}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useJazz } from 'jazz-react';
|
||||
import { Resolved, useJazz } from 'jazz-react';
|
||||
import { Button, ProfilePicImg } from './basicComponents/index.tsx';
|
||||
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
|
||||
import { Queried } from 'cojson';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BrowserImage } from 'jazz-browser-media-images';
|
||||
|
||||
export function FollowButton({ profile }: { profile?: Queried<TwitProfile> }) {
|
||||
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);
|
||||
@@ -32,7 +31,7 @@ export function FollowButton({ profile }: { profile?: Queried<TwitProfile> }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FollowerList({ profile }: { profile?: Queried<TwitProfile> }) {
|
||||
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]) => {
|
||||
@@ -58,7 +57,7 @@ export function FollowerList({ profile }: { profile?: Queried<TwitProfile> }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FollowingList({ profile }: { profile?: Queried<TwitProfile> }) {
|
||||
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 => {
|
||||
|
||||
@@ -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}`],
|
||||
|
||||
3
homepage/homepage-jazz/.eslintrc.json
Normal file
3
homepage/homepage-jazz/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
35
homepage/homepage-jazz/.gitignore
vendored
Normal file
35
homepage/homepage-jazz/.gitignore
vendored
Normal 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
|
||||
36
homepage/homepage-jazz/README.md
Normal file
36
homepage/homepage-jazz/README.md
Normal 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.
|
||||
BIN
homepage/homepage-jazz/app/favicon.ico
Normal file
BIN
homepage/homepage-jazz/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
27
homepage/homepage-jazz/app/globals.css
Normal file
27
homepage/homepage-jazz/app/globals.css
Normal 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));
|
||||
}
|
||||
22
homepage/homepage-jazz/app/layout.tsx
Normal file
22
homepage/homepage-jazz/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
113
homepage/homepage-jazz/app/page.tsx
Normal file
113
homepage/homepage-jazz/app/page.tsx
Normal 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
|
||||
<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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with 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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</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>
|
||||
)
|
||||
}
|
||||
4
homepage/homepage-jazz/next.config.js
Normal file
4
homepage/homepage-jazz/next.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
27
homepage/homepage-jazz/package.json
Normal file
27
homepage/homepage-jazz/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
homepage/homepage-jazz/postcss.config.js
Normal file
6
homepage/homepage-jazz/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
homepage/homepage-jazz/public/next.svg
Normal file
1
homepage/homepage-jazz/public/next.svg
Normal 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 |
1
homepage/homepage-jazz/public/vercel.svg
Normal file
1
homepage/homepage-jazz/public/vercel.svg
Normal 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 |
20
homepage/homepage-jazz/tailwind.config.ts
Normal file
20
homepage/homepage-jazz/tailwind.config.ts
Normal 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
|
||||
27
homepage/homepage-jazz/tsconfig.json
Normal file
27
homepage/homepage-jazz/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
@@ -16,8 +16,8 @@
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.1",
|
||||
"cojson-storage-sqlite": "^0.4.1",
|
||||
"cojson": "^0.4.6",
|
||||
"cojson-storage-sqlite": "^0.4.6",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.1",
|
||||
"cojson": "^0.4.6",
|
||||
"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"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "^0.4.1",
|
||||
"cojson": "^0.4.6",
|
||||
"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": {
|
||||
|
||||
@@ -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.1",
|
||||
"version": "0.4.6",
|
||||
"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": {
|
||||
|
||||
@@ -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") {
|
||||
@@ -62,15 +65,4 @@ export function expectStream(content: CoValue): CoStream {
|
||||
}
|
||||
|
||||
return content as CoStream;
|
||||
}
|
||||
|
||||
export function isCoValue(
|
||||
value: JsonValue | CoValue | undefined
|
||||
): value is CoValue {
|
||||
return (
|
||||
value instanceof CoMap ||
|
||||
value instanceof CoList ||
|
||||
value instanceof CoStream ||
|
||||
value instanceof BinaryCoStream
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
isAccountID,
|
||||
} from "./coValues/account.js";
|
||||
import { Stringified, stableStringify } from "./jsonStringify.js";
|
||||
import { coreToCoValue } from "./coreToCoValue.js";
|
||||
|
||||
export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
|
||||
|
||||
@@ -517,31 +518,7 @@ export class CoValueCore {
|
||||
return this._cachedContent;
|
||||
}
|
||||
|
||||
let newContent;
|
||||
if (this.header.type === "comap") {
|
||||
if (this.header.ruleset.type === "group") {
|
||||
if (
|
||||
this.header.meta?.type === "account" &&
|
||||
!options?.ignorePrivateTransactions
|
||||
) {
|
||||
newContent = new Account(this);
|
||||
} else {
|
||||
newContent = new Group(this, options);
|
||||
}
|
||||
} else {
|
||||
newContent = new CoMap(this);
|
||||
}
|
||||
} else if (this.header.type === "colist") {
|
||||
newContent = new CoList(this);
|
||||
} else if (this.header.type === "costream") {
|
||||
if (this.header.meta && this.header.meta.type === "binary") {
|
||||
newContent = new BinaryCoStream(this);
|
||||
} else {
|
||||
newContent = new CoStream(this);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown coValue type ${this.header.type}`);
|
||||
}
|
||||
const newContent = coreToCoValue(this, options);
|
||||
|
||||
if (!options?.ignorePrivateTransactions) {
|
||||
this._cachedContent = newContent;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoID, CoValue, isCoValue } from "../coValue.js";
|
||||
import { CoID, CoValue } from "../coValue.js";
|
||||
import { isCoValue } from "../isCoValue.js";
|
||||
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
||||
import { AccountID } from "./account.js";
|
||||
@@ -197,7 +198,7 @@ export class CoListView<
|
||||
}
|
||||
|
||||
/** @category 6. Meta */
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { AgentID, TransactionID } from "../ids.js";
|
||||
import { CoID, CoValue, isCoValue } from "../coValue.js";
|
||||
import { CoID, CoValue } from "../coValue.js";
|
||||
import { isCoValue } from "../isCoValue.js";
|
||||
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { AccountID } from "./account.js";
|
||||
import { parseJSON } from "../jsonStringify.js";
|
||||
@@ -84,7 +85,7 @@ export class CoMapView<
|
||||
}
|
||||
|
||||
/** @category 6. Meta */
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoValue, CoID, isCoValue } from "../coValue.js";
|
||||
import { CoValue, CoID } from "../coValue.js";
|
||||
import { isCoValue } from "../isCoValue.js";
|
||||
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { Group } from "./group.js";
|
||||
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
||||
@@ -59,7 +60,7 @@ export class CoStreamView<
|
||||
this.fillFromCoValue();
|
||||
}
|
||||
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
@@ -290,8 +291,6 @@ export class BinaryCoStreamView<
|
||||
extends CoStreamView<BinaryStreamItem, Meta>
|
||||
implements CoValue
|
||||
{
|
||||
id!: CoID<this>;
|
||||
|
||||
getBinaryChunks(
|
||||
allowUnfinished?: boolean
|
||||
):
|
||||
|
||||
@@ -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({
|
||||
|
||||
37
packages/cojson/src/coreToCoValue.ts
Normal file
37
packages/cojson/src/coreToCoValue.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { CoValueCore } from "./coValueCore.js";
|
||||
import { Account } from "./coValues/account.js";
|
||||
import { Group } from "./coValues/group.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 function coreToCoValue(
|
||||
core: CoValueCore,
|
||||
options?: { ignorePrivateTransactions: true }
|
||||
) {
|
||||
if (core.header.type === "comap") {
|
||||
if (core.header.ruleset.type === "group") {
|
||||
if (
|
||||
core.header.meta?.type === "account" &&
|
||||
!options?.ignorePrivateTransactions
|
||||
) {
|
||||
return new Account(core);
|
||||
} else {
|
||||
return new Group(core, options);
|
||||
}
|
||||
} else {
|
||||
return new CoMap(core);
|
||||
}
|
||||
} else if (core.header.type === "colist") {
|
||||
return new CoList(core);
|
||||
} else if (core.header.type === "costream") {
|
||||
if (core.header.meta && core.header.meta.type === "binary") {
|
||||
return new BinaryCoStream(core);
|
||||
} else {
|
||||
return new CoStream(core);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown coValue type ${core.header.type}`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
16
packages/cojson/src/isCoValue.ts
Normal file
16
packages/cojson/src/isCoValue.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { JsonValue } from "./jsonValue.js";
|
||||
import { CoMap } from "./coValues/coMap.js";
|
||||
import { BinaryCoStream, CoStream } from "./coValues/coStream.js";
|
||||
import { CoList } from "./coValues/coList.js";
|
||||
import { CoValue } from "./coValue.js";
|
||||
|
||||
export function isCoValue(
|
||||
value: JsonValue | CoValue | undefined
|
||||
): value is CoValue {
|
||||
return (
|
||||
value instanceof CoMap ||
|
||||
value instanceof CoList ||
|
||||
value instanceof CoStream ||
|
||||
value instanceof BinaryCoStream
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
22
packages/jazz-autosub/.eslintrc.cjs
Normal file
22
packages/jazz-autosub/.eslintrc.cjs
Normal 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
171
packages/jazz-autosub/.gitignore
vendored
Normal 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
|
||||
2
packages/jazz-autosub/.npmignore
Normal file
2
packages/jazz-autosub/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
19
packages/jazz-autosub/LICENSE.txt
Normal file
19
packages/jazz-autosub/LICENSE.txt
Normal 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.
|
||||
3
packages/jazz-autosub/README.md
Normal file
3
packages/jazz-autosub/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# CoJSON
|
||||
|
||||
[See the top-level README](../../README.md#cojson)
|
||||
40
packages/jazz-autosub/package.json
Normal file
40
packages/jazz-autosub/package.json
Normal 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.6",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.6"
|
||||
},
|
||||
"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/"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
BinaryCoStream,
|
||||
CoList,
|
||||
CoMap,
|
||||
CoStream,
|
||||
Group,
|
||||
LocalNode,
|
||||
cojsonReady,
|
||||
} from "..";
|
||||
import { CoList, CoMap, CoStream, Group, LocalNode, cojsonReady } from "cojson";
|
||||
import { autoSub } from ".";
|
||||
|
||||
beforeEach(async () => {
|
||||
await cojsonReady;
|
||||
@@ -30,48 +23,50 @@ test("Queries with maps work", async () => {
|
||||
>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(map.id, (queriedMap) => {
|
||||
const unsubQuery = autoSub(map.id, node, (resolvedMap) => {
|
||||
// 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(
|
||||
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(queriedMap.edits.hello.by.profile.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(queriedMap.edits.hello.by.isMe).toBe(true);
|
||||
expect(queriedMap.edits.hello.tx).toEqual(
|
||||
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(queriedMap.edits.hello.at).toEqual(
|
||||
expect(resolvedMap.meta.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") {
|
||||
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'");
|
||||
queriedMap.subMap.set("hello", "sun");
|
||||
resolvedMap.subMap.set("hello", "sun");
|
||||
} else if (
|
||||
queriedMap.subMap.hello === "sun" &&
|
||||
queriedMap.subMap.edits.hello?.by?.profile?.name ===
|
||||
"Hermes Puggington"
|
||||
resolvedMap.subMap.hello === "sun" &&
|
||||
resolvedMap.subMap.meta.edits.hello?.by?.profile
|
||||
?.name === "Hermes Puggington"
|
||||
) {
|
||||
// console.log("final update", queriedMap);
|
||||
// console.log("final update", resolvedMap);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
@@ -112,37 +107,36 @@ test("Queries with lists work", () => {
|
||||
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(
|
||||
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(queriedList.edits[2].by.profile.name).toEqual(
|
||||
expect(resolvedList.meta.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");
|
||||
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 (
|
||||
queriedList.length === 4 &&
|
||||
queriedList.edits[3]?.by?.profile?.name ===
|
||||
resolvedList.length === 4 &&
|
||||
resolvedList.meta.edits[3]?.by?.profile?.name ===
|
||||
"Hermes Puggington"
|
||||
) {
|
||||
expect(queriedList[3]).toBe("sun");
|
||||
// console.log("final update", queriedList);
|
||||
expect(resolvedList[3]).toBe("sun");
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
@@ -170,14 +164,14 @@ test("List of nested maps works", () => {
|
||||
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({
|
||||
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", queriedList);
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
@@ -203,14 +197,14 @@ test("Can call .map on a quieried coList", async () => {
|
||||
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([
|
||||
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", queriedList);
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
@@ -235,121 +229,120 @@ test("Queries with streams work", () => {
|
||||
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);
|
||||
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(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].last
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[0].value
|
||||
).toEqual("hello");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[0].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][0].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[1].value
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[1].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][1].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.id
|
||||
).toEqual(accountID);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.profile?.id
|
||||
).toEqual(node.expectProfileLoaded(accountID).id);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.profile?.name
|
||||
).toEqual("Hermes Puggington");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.isMe
|
||||
).toBe(true);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perSession)[
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].at
|
||||
).toBeInstanceOf(Date);
|
||||
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.last
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[0].value
|
||||
).toEqual("hello");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[0].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][0].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[1].value
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[1].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][1].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.id
|
||||
).toEqual(accountID);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.profile?.id
|
||||
).toEqual(node.expectProfileLoaded(accountID).id);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.profile?.name
|
||||
).toEqual("Hermes Puggington");
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.isMe
|
||||
).toBe(true);
|
||||
expect(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.at
|
||||
).toBeInstanceOf(Date);
|
||||
|
||||
expect(queriedStream.me).toEqual(
|
||||
Object.fromEntries(queriedStream.perAccount)[accountID]
|
||||
expect(resolvedStream.me).toEqual(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
);
|
||||
// console.log("final update", queriedStream);
|
||||
// console.log("final update", resolvedStream);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
@@ -374,14 +367,14 @@ test("Streams of nested maps work", () => {
|
||||
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({
|
||||
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", queriedList);
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
334
packages/jazz-autosub/src/autoSub.ts
Normal file
334
packages/jazz-autosub/src/autoSub.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
9
packages/jazz-autosub/src/index.ts
Normal file
9
packages/jazz-autosub/src/index.ts
Normal 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";
|
||||
@@ -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
|
||||
);
|
||||
254
packages/jazz-autosub/src/resolvedCoValues/resolvedCoList.ts
Normal file
254
packages/jazz-autosub/src/resolvedCoValues/resolvedCoList.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
171
packages/jazz-autosub/src/resolvedCoValues/resolvedCoMap.ts
Normal file
171
packages/jazz-autosub/src/resolvedCoValues/resolvedCoMap.ts
Normal 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;
|
||||
}
|
||||
@@ -1,50 +1,64 @@
|
||||
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";
|
||||
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 QueriedCoStreamEntry<Item extends JsonValue | CoValue> = {
|
||||
last?: ValueOrSubQueried<Item>;
|
||||
by?: QueriedAccount;
|
||||
tx?: TransactionID;
|
||||
export type ResolvedCoStreamEntry<Item extends JsonValue | CoValue> = {
|
||||
last?: ValueOrResolvedRef<Item>;
|
||||
by?: ResolvedAccount;
|
||||
tx?: CojsonInternalTypes.TransactionID;
|
||||
at?: Date;
|
||||
all: {
|
||||
value: ValueOrSubQueried<Item>;
|
||||
by?: QueriedAccount;
|
||||
tx: TransactionID;
|
||||
value: ValueOrResolvedRef<Item>;
|
||||
by?: ResolvedAccount;
|
||||
tx: CojsonInternalTypes.TransactionID;
|
||||
at: Date;
|
||||
}[];
|
||||
};
|
||||
|
||||
export class QueriedCoStream<S extends CoStream> {
|
||||
coStream!: S;
|
||||
export type ResolvedCoStreamMeta<S extends CoStream> = {
|
||||
coValue: S;
|
||||
headerMeta: S["headerMeta"];
|
||||
group: Group;
|
||||
}
|
||||
|
||||
export class ResolvedCoStream<S extends CoStream> {
|
||||
id: CoID<S>;
|
||||
type = "costream" as const;
|
||||
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, queryContext: QueryContext) {
|
||||
Object.defineProperty(this, "coStream", {
|
||||
get() {
|
||||
return coStream;
|
||||
},
|
||||
});
|
||||
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) =>
|
||||
queryContext.defineSubqueryPropertiesIn(
|
||||
autoSubContext.defineResolvedRefPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: isAccountID(item.by)
|
||||
value: cojsonInternals.isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
@@ -72,20 +86,20 @@ export class QueriedCoStream<S extends CoStream> {
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies QueriedCoStreamEntry<S["_item"]>,
|
||||
} satisfies ResolvedCoStreamEntry<S["_item"]>,
|
||||
];
|
||||
});
|
||||
|
||||
this.perAccount = [...coStream.accounts()].map((accountID) => {
|
||||
const items = [...coStream.itemsBy(accountID)].map((item) =>
|
||||
queryContext.defineSubqueryPropertiesIn(
|
||||
autoSubContext.defineResolvedRefPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: isAccountID(item.by)
|
||||
value: cojsonInternals.isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
@@ -111,41 +125,22 @@ export class QueriedCoStream<S extends CoStream> {
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies QueriedCoStreamEntry<S["_item"]>;
|
||||
} satisfies ResolvedCoStreamEntry<S["_item"]>;
|
||||
|
||||
if (accountID === queryContext.node.account.id) {
|
||||
if (accountID === autoSubContext.node.account.id) {
|
||||
this.me = entry;
|
||||
}
|
||||
|
||||
return [
|
||||
accountID,
|
||||
entry
|
||||
];
|
||||
return [accountID, entry];
|
||||
});
|
||||
}
|
||||
|
||||
get meta(): S["meta"] {
|
||||
return this.coStream.meta;
|
||||
}
|
||||
|
||||
get group(): Group {
|
||||
return this.coStream.group;
|
||||
}
|
||||
|
||||
get core(): CoValueCore {
|
||||
return this.coStream.core;
|
||||
}
|
||||
|
||||
me?: QueriedCoStreamEntry<S["_item"]>;
|
||||
perAccount: [account: AccountID, items: QueriedCoStreamEntry<S["_item"]>][];
|
||||
perSession: [session: SessionID, items: QueriedCoStreamEntry<S["_item"]>][];
|
||||
|
||||
push(item: S["_item"], privacy?: "private" | "trusting"): S {
|
||||
return this.coStream.push(item, privacy);
|
||||
return this.meta.coValue.push(item, privacy);
|
||||
}
|
||||
mutate(
|
||||
mutator: (mutable: MutableCoStream<S["_item"], S["meta"]>) => void
|
||||
mutator: (mutable: MutableCoStream<S["_item"], S["headerMeta"]>) => void
|
||||
): S {
|
||||
return this.coStream.mutate(mutator);
|
||||
return this.meta.coValue.mutate(mutator);
|
||||
}
|
||||
}
|
||||
97
packages/jazz-autosub/src/resolvedCoValues/resolvedGroup.ts
Normal file
97
packages/jazz-autosub/src/resolvedCoValues/resolvedGroup.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
16
packages/jazz-autosub/tsconfig.json
Normal file
16
packages/jazz-autosub/tsconfig.json
Normal 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.*"],
|
||||
}
|
||||
2786
packages/jazz-autosub/yarn.lock
Normal file
2786
packages/jazz-autosub/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-local",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.4.1",
|
||||
"jazz-browser": "^0.4.6",
|
||||
"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"
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.1",
|
||||
"@types/image-blob-reduce": "^4.1.1",
|
||||
"cojson": "^0.4.6",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "^0.4.1",
|
||||
"jazz-autosub": "^0.4.6",
|
||||
"jazz-browser": "^0.4.6",
|
||||
"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": {
|
||||
"@types/image-blob-reduce": "^4.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.1",
|
||||
"cojson-storage-indexeddb": "^0.4.1",
|
||||
"cojson": "^0.4.6",
|
||||
"cojson-storage-indexeddb": "^0.4.6",
|
||||
"jazz-autosub": "^0.4.6",
|
||||
"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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react-auth-local",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.4.1",
|
||||
"jazz-react": "^0.4.1",
|
||||
"jazz-browser-auth-local": "^0.4.6",
|
||||
"jazz-react": "^0.4.6",
|
||||
"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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.1",
|
||||
"jazz-browser": "^0.4.1",
|
||||
"cojson": "^0.4.6",
|
||||
"jazz-browser": "^0.4.6",
|
||||
"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"
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user