Compare commits
14 Commits
jazz-react
...
cojson-sim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee2cad39e | ||
|
|
b7c8a0038b | ||
|
|
8c27e8c379 | ||
|
|
0133aa47ff | ||
|
|
5659c925a2 | ||
|
|
27779ac792 | ||
|
|
3f1bfa4629 | ||
|
|
15a693c3ed | ||
|
|
b1d620e145 | ||
|
|
478fbd0aa9 | ||
|
|
ee906b7351 | ||
|
|
dd15f21ccb | ||
|
|
d7cd5fda7c | ||
|
|
174300b00f |
4
.github/workflows/build-and-deploy.yaml
vendored
4
.github/workflows/build-and-deploy.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["todo", "pets"]
|
||||
example: ["todo", "pets", "twit"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
example: ["todo", "pets"]
|
||||
example: ["todo", "pets", "twit"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
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.15",
|
||||
"version": "0.0.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,9 +16,9 @@
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "^0.4.0",
|
||||
"jazz-react": "^0.4.0",
|
||||
"jazz-react-auth-local": "^0.4.0",
|
||||
"jazz-browser-media-images": "^0.4.5",
|
||||
"jazz-react": "^0.4.5",
|
||||
"jazz-react-auth-local": "^0.4.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -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,16 +73,16 @@ export function RatePetPostUI() {
|
||||
function ReactionOverview({
|
||||
petReactions,
|
||||
}: {
|
||||
petReactions: Queried<PetReactions>;
|
||||
petReactions: Resolved<PetReactions>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Reactions</h2>
|
||||
<div className="flex flex-col gap-1">
|
||||
{REACTION_TYPES.map((reactionType) => {
|
||||
const reactionsOfThisType = Object.values(
|
||||
petReactions.perAccount
|
||||
).filter(({ last }) => last === reactionType);
|
||||
const reactionsOfThisType = petReactions.perAccount
|
||||
.map(([, reaction]) => reaction)
|
||||
.filter(({ last }) => last === reactionType);
|
||||
|
||||
if (reactionsOfThisType.length === 0) return null;
|
||||
|
||||
|
||||
@@ -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.40",
|
||||
"version": "0.0.42",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "^0.4.0",
|
||||
"jazz-react-auth-local": "^0.4.0",
|
||||
"jazz-react": "^0.4.5",
|
||||
"jazz-react-auth-local": "^0.4.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -9,10 +9,9 @@ import { SubmittableInput } from "./basicComponents";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export function NewProjectForm() {
|
||||
// A `LocalNode` represents a local view of loaded & created CoValues.
|
||||
// It is associated with a current user account, which will determine
|
||||
// `me` represents the current user account, which will determine
|
||||
// access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
const { localNode } = useJazz();
|
||||
const { me } = useJazz();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createProject = useCallback(
|
||||
@@ -22,7 +21,7 @@ export function NewProjectForm() {
|
||||
// To create a new todo project, we first create a `Group`,
|
||||
// which is a scope for defining access rights (reader/writer/admin)
|
||||
// of its members, which will apply to all CoValues owned by that group.
|
||||
const projectGroup = localNode.createGroup();
|
||||
const projectGroup = me.createGroup();
|
||||
|
||||
// Then we create an empty todo project within that group
|
||||
const project = projectGroup.createMap<TodoProject>({
|
||||
@@ -32,7 +31,7 @@ export function NewProjectForm() {
|
||||
|
||||
navigate("/project/" + project.id);
|
||||
},
|
||||
[localNode, navigate]
|
||||
[me, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
job "example-todo$BRANCH_SUFFIX" {
|
||||
job "twit$BRANCH_SUFFIX" {
|
||||
region = "global"
|
||||
datacenters = ["*"]
|
||||
|
||||
@@ -41,7 +41,7 @@ job "example-todo$BRANCH_SUFFIX" {
|
||||
|
||||
service {
|
||||
tags = ["public"]
|
||||
name = "example-todo$BRANCH_SUFFIX"
|
||||
name = "twit$BRANCH_SUFFIX"
|
||||
port = "http"
|
||||
provider = "consul"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-twit",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,15 +11,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"javascript-time-ago": "^2.5.9",
|
||||
"jazz-browser-media-images": "^0.4.0",
|
||||
"jazz-react": "^0.4.0",
|
||||
"jazz-react-auth-local": "^0.4.0",
|
||||
"jazz-browser-media-images": "^0.4.5",
|
||||
"jazz-react": "^0.4.5",
|
||||
"jazz-react-auth-local": "^0.4.5",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import { CoMap, CoList, Media, CoStream, Group, ProfileMeta, AccountMigration, EVERYONE } from 'cojson';
|
||||
import { CoMap, CoList, Media, CoStream, Group, AccountMigration, EVERYONE, Profile } from 'cojson';
|
||||
|
||||
export type Twit = CoMap<{
|
||||
text?: string;
|
||||
images?: ListOfImages['id'];
|
||||
likes: LikeStream['id'];
|
||||
quotedPost?: Twit['id'];
|
||||
replies: ReplyStream['id'];
|
||||
isRepostedIn: RepostedInStream['id'];
|
||||
isReplyTo?: Twit['id'];
|
||||
}>;
|
||||
|
||||
export type ListOfImages = CoList<Media.ImageDefinition['id']>;
|
||||
export type LikeStream = CoStream<'❤️' | null>;
|
||||
export type ReplyStream = CoStream<Twit['id']>;
|
||||
export type RepostedInStream = CoStream<Twit['id']>;
|
||||
|
||||
export type ListOfTwits = CoList<Twit['id']>;
|
||||
export type ListOfProfiles = CoList<TwitProfile['id']>;
|
||||
export type StreamOfFollowers = CoStream<TwitProfile['id'] | null>;
|
||||
|
||||
export type TwitProfile = CoMap<
|
||||
export type TwitProfile = Profile<
|
||||
{
|
||||
name: string;
|
||||
bio: string;
|
||||
avatar?: Media.ImageDefinition['id'];
|
||||
twits: ListOfTwits['id'];
|
||||
following: ListOfProfiles['id'];
|
||||
followers: ListOfProfiles['id'];
|
||||
followers: StreamOfFollowers['id'];
|
||||
twitStyle?: { fontFamily: string; color: string };
|
||||
},
|
||||
ProfileMeta
|
||||
}
|
||||
>;
|
||||
|
||||
export type TwitAccountRoot = CoMap<{
|
||||
@@ -61,7 +58,7 @@ export const migration: AccountMigration<TwitProfile, TwitAccountRoot> = (accoun
|
||||
|
||||
profile.set('twits', peopleWhoCanSeeMyTwits.createList<ListOfTwits>().id, 'trusting');
|
||||
profile.set('following', peopleWhoCanSeeMyFollows.createList<ListOfProfiles>().id, 'trusting');
|
||||
profile.set('followers', peopleWhoCanFollowMe.createList<ListOfProfiles>().id, 'trusting');
|
||||
profile.set('followers', peopleWhoCanFollowMe.createStream<StreamOfFollowers>().id, 'trusting');
|
||||
console.log('MIGRATION SUCCESSFUL!');
|
||||
}
|
||||
};
|
||||
@@ -1,49 +1,18 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Link, RouterProvider, createHashRouter, useParams } from 'react-router-dom';
|
||||
import { RouterProvider, createHashRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
|
||||
import { WithJazz, useJazz, useSyncedQuery } from 'jazz-react';
|
||||
import { AccountMigration } from 'cojson';
|
||||
import { WithJazz, useJazz } from 'jazz-react';
|
||||
import { LocalAuth } from 'jazz-react-auth-local';
|
||||
|
||||
import {
|
||||
AddTwitPicsInput,
|
||||
BioInput,
|
||||
Button,
|
||||
ButtonWithCount,
|
||||
ChooseProfilePicInput,
|
||||
FollowerStatsContainer,
|
||||
LargeProfilePicImg,
|
||||
ProfileName,
|
||||
ProfilePicImg,
|
||||
ProfileTitleContainer,
|
||||
ReactionsAndReplyContainer,
|
||||
ReactionsContainer,
|
||||
RepliesContainer,
|
||||
SubtleProfileID,
|
||||
SubtleRelativeTimeAgo,
|
||||
ThemeProvider,
|
||||
TitleAndLogo,
|
||||
TwitImg,
|
||||
TwitTextInput
|
||||
} from './basicComponents/index.tsx';
|
||||
import { Button, ThemeProvider, TitleAndLogo } from './basicComponents/index.tsx';
|
||||
import { PrettyAuthUI } from './components/Auth.tsx';
|
||||
import {
|
||||
LikeStream,
|
||||
ListOfImages,
|
||||
ReplyStream,
|
||||
RepostedInStream,
|
||||
Twit,
|
||||
TwitAccountRoot,
|
||||
TwitProfile,
|
||||
migration
|
||||
} from './1_types.ts';
|
||||
import { AccountMigration, CoID, Queried } from 'cojson';
|
||||
import { BrowserImage, createImage } from 'jazz-browser-media-images';
|
||||
import TimeAgo from 'javascript-time-ago';
|
||||
import en from 'javascript-time-ago/locale/en.json';
|
||||
import { HeartIcon, MessagesSquareIcon } from 'lucide-react';
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
|
||||
import { migration } from './1_dataModel.ts';
|
||||
import { ChronoFeed } from './3_ChronoFeed.tsx';
|
||||
import { ProfilePage } from './5_ProfilePage.tsx';
|
||||
|
||||
const appName = 'Jazz Twit Example';
|
||||
|
||||
@@ -65,30 +34,21 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
/**
|
||||
* Routing in `<App/>`
|
||||
*
|
||||
* <App> is the main app component, handling client-side routing based
|
||||
* on the CoValue ID (CoID) of our TodoProject, stored in the URL hash
|
||||
* - which can also contain invite links.
|
||||
*/
|
||||
|
||||
function App() {
|
||||
// logOut logs out the AuthProvider passed to `<WithJazz/>` above.
|
||||
const { logOut } = useJazz();
|
||||
const { me, logOut } = useJazz();
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <ChronoFeedUI />
|
||||
},
|
||||
{
|
||||
path: '/me',
|
||||
element: <MyProfile />
|
||||
element: <ChronoFeed />
|
||||
},
|
||||
{
|
||||
path: '/:profileId',
|
||||
element: <UserProfile />
|
||||
element: <ProfilePage />
|
||||
},
|
||||
{
|
||||
path: '/me',
|
||||
loader: () => router.navigate('/' + me.profile?.id)
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -109,283 +69,3 @@ function App() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function MyProfile() {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
return me.profile && <ProfileUI profileId={me.profile.id} />;
|
||||
}
|
||||
|
||||
export function UserProfile() {
|
||||
const { profileId } = useParams<{ profileId: CoID<TwitProfile> }>();
|
||||
|
||||
return profileId && <ProfileUI profileId={profileId} />;
|
||||
}
|
||||
|
||||
export function ProfileUI({ profileId }: { profileId: CoID<TwitProfile> }) {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const profile = useSyncedQuery(profileId);
|
||||
const isMe = profile?.id == me.profile?.id;
|
||||
|
||||
const profileTwitsAndRepliedToTwits = useMemo(() => {
|
||||
return profile?.twits?.map((twit, _, allTwits) =>
|
||||
twit?.isReplyTo
|
||||
? allTwits.some(
|
||||
tw =>
|
||||
tw?.id === twit?.isReplyTo?.id ||
|
||||
tw?.id === twit?.isReplyTo?.isReplyTo?.id ||
|
||||
tw?.id === twit?.isReplyTo?.isReplyTo?.isReplyTo?.id
|
||||
)
|
||||
? null
|
||||
: twit?.isReplyTo
|
||||
: twit
|
||||
);
|
||||
}, [profile?.twits]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-2 mb-5 flex gap-2">
|
||||
<div className="flex flex-col items-stretch">
|
||||
<LargeProfilePicImg src={profile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder} />
|
||||
{isMe && (
|
||||
<ChooseProfilePicInput
|
||||
onChange={(file: File) =>
|
||||
me.root?.peopleWhoCanSeeMyTwits &&
|
||||
createImage(file, me.root.peopleWhoCanSeeMyTwits.group, 256).then(image => {
|
||||
me.profile?.set({ avatar: image.id }, 'trusting');
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grow">
|
||||
<ProfileTitleContainer>
|
||||
<ProfileName>{profile?.name}</ProfileName>
|
||||
<div className="ml-2 text-neutral-300 text-xs">{profile?.id}</div>
|
||||
{!isMe && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!profile?.followers || !me.profile?.following) return;
|
||||
if (profile.followers.some(f => f?.id === me.profile?.id)) {
|
||||
me.profile.following.append(profile.id);
|
||||
profile.followers.append(me.profile.id);
|
||||
} else {
|
||||
me.profile.following.delete(me.profile.following.findIndex(f => f?.id === profile.id));
|
||||
profile.followers.delete(profile.followers.findIndex(f => f?.id === me.profile?.id));
|
||||
}
|
||||
}}
|
||||
className="ml-auto"
|
||||
>
|
||||
{profile?.followers?.some(f => f?.id === me.profile?.id) ? 'Unfollow' : 'Follow'}
|
||||
</Button>
|
||||
)}
|
||||
</ProfileTitleContainer>
|
||||
|
||||
<div>
|
||||
{isMe ? (
|
||||
<BioInput
|
||||
value={profile?.bio}
|
||||
onChange={newBio => {
|
||||
profile?.set({ bio: newBio }, 'trusting');
|
||||
// prettier-ignore
|
||||
if (newBio.startsWith('{')) {profile?.set('twitStyle', JSON.parse(newBio), 'trusting');} else {profile?.set('twitStyle', undefined, 'trusting');}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
profile?.bio || '(No bio)'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FollowerStatsContainer>
|
||||
{new Set(profile?.followers || []).size} Followers — {new Set(profile?.following || []).size}{' '}
|
||||
Following
|
||||
</FollowerStatsContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMe && <CreateTwitForm className="mb-4" />}
|
||||
|
||||
{profileTwitsAndRepliedToTwits?.map(twit => twit && <TwitUI twit={twit} key={twit?.id} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwitUI({
|
||||
twit,
|
||||
alreadyInReplies: alreadyInReplies
|
||||
}: {
|
||||
twit?: Queried<Twit>;
|
||||
alreadyInReplies?: boolean;
|
||||
}) {
|
||||
const [showReplyForm, setShowReplyForm] = React.useState(false);
|
||||
|
||||
const posterProfile = twit?.edits.text?.by?.profile as Queried<TwitProfile> | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'py-2 flex flex-col items-stretch' + (twit?.isReplyTo && !alreadyInReplies ? ' ml-14' : ' border-t')}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<ProfilePicImg
|
||||
src={posterProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
|
||||
smaller={!!twit?.isReplyTo}
|
||||
/>
|
||||
<div className="grow flex flex-col items-stretch">
|
||||
<div className="flex items-baseline">
|
||||
{posterProfile && (
|
||||
<Link to={'/' + posterProfile.id} className="font-bold">
|
||||
{posterProfile.name}
|
||||
</Link>
|
||||
)}
|
||||
<SubtleProfileID>{posterProfile?.id}</SubtleProfileID>
|
||||
<SubtleRelativeTimeAgo dateTime={twit?.edits.text?.at} />
|
||||
</div>
|
||||
<div style={posterProfile?.twitStyle}>{twit?.text}</div>
|
||||
{twit?.quotedPost && (
|
||||
<div className="border rounded">
|
||||
<TwitUI twit={twit.quotedPost} />
|
||||
</div>
|
||||
)}
|
||||
{twit?.images && (
|
||||
<div className="flex gap-2 mt-2 max-w-full overflow-auto">
|
||||
{twit.images.map(image => (
|
||||
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ReactionsAndReplyContainer>
|
||||
<ReactionsContainer>
|
||||
<ButtonWithCount
|
||||
active={twit?.likes?.me?.last === '❤️'}
|
||||
onClick={() => twit?.likes?.push(twit?.likes?.me?.last ? null : '❤️')}
|
||||
count={Object.values(twit?.likes?.perAccount || {}).filter(byAccount => byAccount.last === '❤️').length}
|
||||
icon={<HeartIcon size="18" />}
|
||||
activeIcon={<HeartIcon color="red" size="18" fill="red" />}
|
||||
/>
|
||||
<ButtonWithCount
|
||||
onClick={() => setShowReplyForm(s => !s)}
|
||||
count={Object.values(twit?.replies?.perSession || {}).flatMap(perSession => perSession.all).length}
|
||||
icon={<MessagesSquareIcon size="18" />}
|
||||
/>
|
||||
</ReactionsContainer>
|
||||
</ReactionsAndReplyContainer>
|
||||
{showReplyForm && (
|
||||
<CreateTwitForm inReplyTo={twit} onSubmit={() => setShowReplyForm(false)} className="mt-5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RepliesContainer>
|
||||
{Object.values(twit?.replies?.perAccount || {})
|
||||
.flatMap(byAccount => byAccount.all)
|
||||
.sort((a, b) => b.at.getTime() - a.at.getTime())
|
||||
.map(replyEntry => (
|
||||
<TwitUI twit={replyEntry.value} key={replyEntry.value?.id} alreadyInReplies={!!twit?.isReplyTo} />
|
||||
))}
|
||||
</RepliesContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChronoFeedUI() {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const myTwits = me.profile?.twits;
|
||||
|
||||
const twitsFromFollows = useMemo(
|
||||
() => me.profile?.following?.flatMap(follow => follow?.twits || []) || [],
|
||||
[me.profile?.following]
|
||||
);
|
||||
|
||||
const allTwitsSorted = useMemo(
|
||||
() =>
|
||||
[...(myTwits || []), ...twitsFromFollows]
|
||||
.flatMap(tw => (tw ? (tw.isReplyTo ? [] : tw) : []))
|
||||
.sort((a, b) => (b.edits.text?.at?.getTime() || 0) - (a.edits.text?.at?.getTime() || 0)),
|
||||
[myTwits, twitsFromFollows]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-stretch">
|
||||
<CreateTwitForm className="mb-10" />
|
||||
<h1 className="text-2xl mb-4">From people you follow</h1>
|
||||
{allTwitsSorted?.map(twit => (
|
||||
<TwitUI twit={twit} key={twit.id} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CreateTwitForm(
|
||||
props: {
|
||||
inReplyTo?: Queried<Twit>;
|
||||
onSubmit?: () => void;
|
||||
className?: string;
|
||||
} = {}
|
||||
) {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const [pics, setPics] = React.useState<File[]>([]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(twitText: string) => {
|
||||
const audience = me.root?.peopleWhoCanSeeMyTwits;
|
||||
const interactors = me.root?.peopleWhoCanInteractWithMe;
|
||||
if (!audience || !interactors) return;
|
||||
|
||||
const images = pics.length ? audience.createList<ListOfImages>() : undefined;
|
||||
|
||||
const twit = audience.createMap<Twit>({
|
||||
text: twitText,
|
||||
likes: interactors.createStream<LikeStream>().id,
|
||||
replies: interactors.createStream<ReplyStream>().id,
|
||||
isRepostedIn: interactors.createStream<RepostedInStream>().id,
|
||||
images: images?.id
|
||||
});
|
||||
|
||||
me.profile?.twits?.prepend(twit?.id as Twit['id']);
|
||||
|
||||
if (props.inReplyTo) {
|
||||
props.inReplyTo.replies?.push(twit.id);
|
||||
twit.set({ isReplyTo: props.inReplyTo.id });
|
||||
}
|
||||
|
||||
pics.forEach(pic => {
|
||||
createImage(pic, twit.group, 1024).then(image => {
|
||||
images!.append(image.id);
|
||||
});
|
||||
});
|
||||
|
||||
setPics([]);
|
||||
props.onSubmit?.();
|
||||
},
|
||||
[me.profile?.twits, me.root?.peopleWhoCanSeeMyTwits, me.root?.peopleWhoCanInteractWithMe, props, pics]
|
||||
);
|
||||
|
||||
const [picPreviews, setPicPreviews] = React.useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
const previews = pics.map(pic => URL.createObjectURL(pic));
|
||||
setPicPreviews(previews);
|
||||
return () => previews.forEach(preview => URL.revokeObjectURL(preview));
|
||||
}, [pics]);
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<TwitTextInput onSubmit={onSubmit} submitButtonLabel={props.inReplyTo ? 'Reply!' : 'Twit!'} />
|
||||
|
||||
{picPreviews.length ? (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{picPreviews.map(preview => (
|
||||
<TwitImg src={preview} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<AddTwitPicsInput
|
||||
onChange={(newPics: File[]) => {
|
||||
setPics(newPics);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
35
examples/twit/src/3_ChronoFeed.tsx
Normal file
35
examples/twit/src/3_ChronoFeed.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useJazz } from 'jazz-react';
|
||||
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
|
||||
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
|
||||
import { TwitComponent } from './4_TwitComponent.tsx';
|
||||
import { MainH1 } from './basicComponents/index.tsx';
|
||||
|
||||
export function ChronoFeed() {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const myTwits = me.profile?.twits;
|
||||
|
||||
const twitsFromFollows = useMemo(
|
||||
() => me.profile?.following?.flatMap(follow => follow?.twits || []) || [],
|
||||
[me.profile?.following]
|
||||
);
|
||||
|
||||
const allTwitsSorted = useMemo(
|
||||
() =>
|
||||
[...(myTwits || []), ...twitsFromFollows]
|
||||
.flatMap(tw => (tw ? (tw.isReplyTo ? [] : tw) : []))
|
||||
.sort((a, b) => (b.meta.edits.text?.at?.getTime() || 0) - (a.meta.edits.text?.at?.getTime() || 0)),
|
||||
[myTwits, twitsFromFollows]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-stretch">
|
||||
<CreateTwitForm className="mb-10" />
|
||||
<MainH1>From people you follow</MainH1>
|
||||
{allTwitsSorted?.map(twit => (
|
||||
<TwitComponent twit={twit} key={twit.id} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
examples/twit/src/4_TwitComponent.tsx
Normal file
101
examples/twit/src/4_TwitComponent.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ButtonWithCount,
|
||||
ProfilePicImg,
|
||||
ReactionsContainer,
|
||||
RepliesContainer,
|
||||
SubtleRelativeTimeAgo,
|
||||
TwitContainer,
|
||||
TwitWithRepliesContainer,
|
||||
TwitImg,
|
||||
TwitImgGallery,
|
||||
TwitHeader,
|
||||
TwitBody,
|
||||
TwitText,
|
||||
} from './basicComponents/index.tsx';
|
||||
import { Twit, TwitProfile } from './1_dataModel.ts';
|
||||
import { BrowserImage } from 'jazz-browser-media-images';
|
||||
import { HeartIcon, MessagesSquareIcon } from 'lucide-react';
|
||||
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
|
||||
import { Resolved } from 'jazz-react';
|
||||
|
||||
export function TwitComponent({
|
||||
twit,
|
||||
alreadyInReplies: alreadyInReplies
|
||||
}: {
|
||||
twit?: Resolved<Twit>;
|
||||
alreadyInReplies?: boolean;
|
||||
}) {
|
||||
const [showReplyForm, setShowReplyForm] = React.useState(false);
|
||||
|
||||
const posterProfile = twit?.meta.edits.text?.by?.profile as Resolved<TwitProfile> | undefined;
|
||||
const isTopLevel = !twit?.isReplyTo || alreadyInReplies;
|
||||
|
||||
return (
|
||||
<TwitWithRepliesContainer isTopLevel={isTopLevel}>
|
||||
<TwitContainer>
|
||||
<ProfilePicImg
|
||||
src={posterProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
|
||||
linkTo={'/' + posterProfile?.id}
|
||||
initial={posterProfile?.name[0]}
|
||||
size={twit?.isReplyTo && "sm"}
|
||||
/>
|
||||
|
||||
<TwitBody>
|
||||
<TwitHeader>
|
||||
<Link to={'/' + posterProfile?.id} className="font-bold hover:underline">
|
||||
{posterProfile?.name}
|
||||
</Link>
|
||||
<SubtleRelativeTimeAgo dateTime={twit?.meta.edits.text?.at} />
|
||||
</TwitHeader>
|
||||
|
||||
<TwitText style={posterProfile?.twitStyle}>
|
||||
{/* This is where the tweet text goes */}
|
||||
{twit?.text}
|
||||
</TwitText>
|
||||
|
||||
{twit?.images && (
|
||||
<TwitImgGallery>
|
||||
{twit.images.map(image => (
|
||||
<TwitImg src={image?.as(BrowserImage)?.highestResSrcOrPlaceholder} key={image?.id} />
|
||||
))}
|
||||
</TwitImgGallery>
|
||||
)}
|
||||
|
||||
<ReactionsContainer>
|
||||
<ButtonWithCount
|
||||
active={twit?.likes?.me?.last === '❤️'}
|
||||
onClick={() => twit?.likes?.push(twit?.likes?.me?.last ? null : '❤️')}
|
||||
count={twit?.likes?.perAccount.filter(([, liked]) => liked.last === '❤️').length || 0}
|
||||
icon={<HeartIcon size="18" />}
|
||||
activeIcon={<HeartIcon color="red" size="18" fill="red" />}
|
||||
/>
|
||||
<ButtonWithCount
|
||||
onClick={() => setShowReplyForm(s => !s)}
|
||||
count={twit?.replies?.perAccount.flatMap(([, byAccount]) => byAccount.all).length || 0}
|
||||
icon={<MessagesSquareIcon size="18" />}
|
||||
/>
|
||||
</ReactionsContainer>
|
||||
</TwitBody>
|
||||
</TwitContainer>
|
||||
|
||||
<RepliesContainer>
|
||||
{showReplyForm && (
|
||||
<CreateTwitForm
|
||||
inReplyTo={twit}
|
||||
onSubmit={() => setShowReplyForm(false)}
|
||||
className={'mt-5 ' + (isTopLevel ? 'ml-14' : 'ml-12')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{twit?.replies?.perAccount
|
||||
.flatMap(([, byAccount]) => byAccount.all)
|
||||
.sort((a, b) => b.at.getTime() - a.at.getTime())
|
||||
.map(replyEntry => (
|
||||
<TwitComponent twit={replyEntry.value} key={replyEntry.value?.id} alreadyInReplies={!!twit?.isReplyTo} />
|
||||
))}
|
||||
</RepliesContainer>
|
||||
</TwitWithRepliesContainer>
|
||||
);
|
||||
}
|
||||
127
examples/twit/src/5_ProfilePage.tsx
Normal file
127
examples/twit/src/5_ProfilePage.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useJazz, useAutoSub } from 'jazz-react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
BioInput,
|
||||
ChooseProfilePicInput,
|
||||
FollowerStatsContainer,
|
||||
Popover,
|
||||
ProfileName,
|
||||
ProfilePicImg,
|
||||
ProfileTitleContainer,
|
||||
SmallInlineButton
|
||||
} from './basicComponents/index.tsx';
|
||||
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
|
||||
import { CoID } from 'cojson';
|
||||
import { BrowserImage, createImage } from 'jazz-browser-media-images';
|
||||
import { FollowButton, FollowerList, FollowingList } from './7_FollowStuff.tsx';
|
||||
import { CreateTwitForm } from './6_CreateTwitForm.tsx';
|
||||
import { TwitComponent } from './4_TwitComponent.tsx';
|
||||
import { PopoverContent, PopoverTrigger } from '@radix-ui/react-popover';
|
||||
|
||||
export function ProfilePage() {
|
||||
const { profileId } = useParams<{ profileId: CoID<TwitProfile> }>();
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const profile = useAutoSub(profileId);
|
||||
const isMe = profile?.id == me.profile?.id;
|
||||
|
||||
const profileTwitsAndRepliedToTwits = useMemo(() => {
|
||||
return profile?.twits?.map((twit, _, allTwits) =>
|
||||
twit?.isReplyTo
|
||||
? allTwits.some(
|
||||
tw =>
|
||||
tw?.id === twit?.isReplyTo?.id ||
|
||||
tw?.id === twit?.isReplyTo?.isReplyTo?.id ||
|
||||
tw?.id === twit?.isReplyTo?.isReplyTo?.isReplyTo?.id
|
||||
)
|
||||
? null
|
||||
: twit?.isReplyTo
|
||||
: twit
|
||||
);
|
||||
}, [profile?.twits]);
|
||||
|
||||
const [qr, setQr] = useState<string>('');
|
||||
useEffect(() => {
|
||||
QRCode.toDataURL(
|
||||
window.location.protocol + '//' + window.location.host + window.location.pathname + '#/' + profile?.id,
|
||||
{
|
||||
errorCorrectionLevel: 'L'
|
||||
}
|
||||
).then(setQr);
|
||||
}, [profile?.id]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-2 mb-5 flex gap-4">
|
||||
<div className="flex flex-col items-stretch">
|
||||
<ProfilePicImg
|
||||
src={profile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
|
||||
initial={profile?.name[0]}
|
||||
size="xxl"
|
||||
/>
|
||||
{isMe && (
|
||||
<ChooseProfilePicInput
|
||||
onChange={(file: File) =>
|
||||
me.root?.peopleWhoCanSeeMyTwits &&
|
||||
createImage(file, me.root.peopleWhoCanSeeMyTwits, 256).then(image => {
|
||||
me.profile?.set({ avatar: image.id }, 'trusting');
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grow">
|
||||
<ProfileTitleContainer>
|
||||
<ProfileName>{profile?.name}</ProfileName>
|
||||
{!isMe && <FollowButton profile={profile} />}
|
||||
</ProfileTitleContainer>
|
||||
|
||||
<div>
|
||||
{isMe ? (
|
||||
<BioInput
|
||||
value={profile?.bio}
|
||||
onChange={newBio => {
|
||||
profile?.set({ bio: newBio }, 'trusting');
|
||||
// prettier-ignore
|
||||
if (newBio.startsWith('{')) { profile?.set('twitStyle', JSON.parse(newBio), 'trusting'); } else { profile?.set('twitStyle', undefined, 'trusting'); }
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
profile?.bio || '(No bio)'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FollowerStatsContainer>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<SmallInlineButton>
|
||||
{profile?.followers?.perAccount?.filter(([, status]) => status.last).length} Followers
|
||||
</SmallInlineButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<FollowerList profile={profile} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<span className="hidden md:block">—</span> <br className="md:hidden" />
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<SmallInlineButton>{new Set(profile?.following || []).size} Following</SmallInlineButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<FollowingList profile={profile} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</FollowerStatsContainer>
|
||||
</div>
|
||||
|
||||
{isMe && <img src={qr} className="rounded w-28 h-28 -mr-3 dark:invert max-sm:w-16 max-sm:h-16" />}
|
||||
</div>
|
||||
|
||||
{isMe && <CreateTwitForm className="mb-4" />}
|
||||
|
||||
{profileTwitsAndRepliedToTwits?.map(twit => twit && <TwitComponent twit={twit} key={twit?.id} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
examples/twit/src/6_CreateTwitForm.tsx
Normal file
73
examples/twit/src/6_CreateTwitForm.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Resolved, useJazz } from 'jazz-react';
|
||||
import { AddTwitPicsInput, TwitImg, TwitTextInput } from './basicComponents/index.tsx';
|
||||
import { LikeStream, ListOfImages, ReplyStream, Twit, TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
|
||||
import { createImage } from 'jazz-browser-media-images';
|
||||
|
||||
export function CreateTwitForm(
|
||||
props: {
|
||||
inReplyTo?: Resolved<Twit>;
|
||||
onSubmit?: () => void;
|
||||
className?: string;
|
||||
} = {}
|
||||
) {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const [pics, setPics] = React.useState<File[]>([]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(twitText: string) => {
|
||||
const audience = me.root?.peopleWhoCanSeeMyTwits;
|
||||
const interactors = me.root?.peopleWhoCanInteractWithMe;
|
||||
if (!audience || !interactors) return;
|
||||
|
||||
const twit = audience.createMap<Twit>({
|
||||
text: twitText,
|
||||
likes: interactors.createStream<LikeStream>().id,
|
||||
replies: interactors.createStream<ReplyStream>().id
|
||||
});
|
||||
|
||||
me.profile?.twits?.prepend(twit?.id as Twit['id']);
|
||||
|
||||
if (props.inReplyTo) {
|
||||
props.inReplyTo.replies?.push(twit.id);
|
||||
twit.set({ isReplyTo: props.inReplyTo.id });
|
||||
}
|
||||
|
||||
Promise.all(pics.map(pic => createImage(pic, twit.group, 1024))).then(createdPics => {
|
||||
twit.set({ images: audience.createList<ListOfImages>(createdPics.map(pic => pic.id)).id });
|
||||
});
|
||||
|
||||
setPics([]);
|
||||
props.onSubmit?.();
|
||||
},
|
||||
[me.profile?.twits, me.root?.peopleWhoCanSeeMyTwits, me.root?.peopleWhoCanInteractWithMe, props, pics]
|
||||
);
|
||||
|
||||
const [picPreviews, setPicPreviews] = React.useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
const previews = pics.map(pic => URL.createObjectURL(pic));
|
||||
setPicPreviews(previews);
|
||||
return () => previews.forEach(preview => URL.revokeObjectURL(preview));
|
||||
}, [pics]);
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<TwitTextInput onSubmit={onSubmit} submitButtonLabel={props.inReplyTo ? 'Reply!' : 'Twit!'} />
|
||||
|
||||
{picPreviews.length ? (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{picPreviews.map(preview => (
|
||||
<TwitImg src={preview} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<AddTwitPicsInput
|
||||
onChange={(newPics: File[]) => {
|
||||
setPics(newPics);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
examples/twit/src/7_FollowStuff.tsx
Normal file
80
examples/twit/src/7_FollowStuff.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Resolved, useJazz } from 'jazz-react';
|
||||
import { Button, ProfilePicImg } from './basicComponents/index.tsx';
|
||||
import { TwitAccountRoot, TwitProfile } from './1_dataModel.ts';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { BrowserImage } from 'jazz-browser-media-images';
|
||||
|
||||
export function FollowButton({ profile }: { profile?: Resolved<TwitProfile> }) {
|
||||
const { me } = useJazz<TwitProfile, TwitAccountRoot>();
|
||||
|
||||
const alreadyFollowing = profile?.followers?.perAccount?.some(([acc, status]) => acc === me.id && !!status.last);
|
||||
const theyFollowMe = profile?.following?.some(f => f?.id === me.profile?.id);
|
||||
|
||||
const followOrUnfollow = useCallback(() => {
|
||||
if (!profile?.followers || !me.profile?.following) return;
|
||||
if (alreadyFollowing) {
|
||||
me.profile.following.delete(me.profile.following.findIndex(f => f?.id === profile.id));
|
||||
profile.followers.push(null);
|
||||
} else {
|
||||
me.profile.following.append(profile.id);
|
||||
profile.followers.push(me.profile.id);
|
||||
}
|
||||
}, [alreadyFollowing, me.profile, profile]);
|
||||
|
||||
return profile?.id === me.profile?.id ? (
|
||||
<div className="ml-auto text-neutral-500">That's you!</div>
|
||||
) : (
|
||||
<Button onClick={followOrUnfollow} className="ml-auto" variant={alreadyFollowing ? 'ghost' : 'default'}>
|
||||
{alreadyFollowing ? 'Unfollow' : theyFollowMe ? 'Follow Back' : 'Follow'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function FollowerList({ profile }: { profile?: Resolved<TwitProfile> }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4 bg-background rounded-lg border shadow-lg w-96 max-w-full m-2">
|
||||
{profile?.followers?.perAccount.map(([, followEntry]) => {
|
||||
const followerProfile = followEntry.last;
|
||||
// not following anymore?
|
||||
if (!followerProfile) return null;
|
||||
|
||||
return (
|
||||
<div key={followerProfile.id} className="flex items-center">
|
||||
<ProfilePicImg
|
||||
src={followerProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
|
||||
linkTo={'/' + followerProfile?.id}
|
||||
initial={followerProfile?.name[0]}
|
||||
/>
|
||||
<Link to={'/' + followerProfile?.id} className="font-bold hover:underline">
|
||||
{followerProfile?.name}
|
||||
</Link>
|
||||
<FollowButton profile={followerProfile} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FollowingList({ profile }: { profile?: Resolved<TwitProfile> }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4 bg-background rounded-lg border shadow-lg w-96 max-w-full m-2">
|
||||
{[...new Set(profile?.following || [])].map(followingProfile => {
|
||||
return (
|
||||
<div key={followingProfile?.id} className="flex items-center">
|
||||
<ProfilePicImg
|
||||
src={followingProfile?.avatar?.as(BrowserImage)?.highestResSrcOrPlaceholder}
|
||||
linkTo={'/' + followingProfile?.id}
|
||||
initial={followingProfile?.name[0]}
|
||||
/>
|
||||
<Link to={'/' + followingProfile?.id} className="font-bold hover:underline">
|
||||
{followingProfile?.name}
|
||||
</Link>
|
||||
<FollowButton profile={followingProfile} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
import { Button } from './ui/button';
|
||||
import { Button, ButtonProps } from './ui/button';
|
||||
export { Button } from './ui/button';
|
||||
export { Checkbox } from './ui/checkbox';
|
||||
import { Input } from './ui/input';
|
||||
import { Link } from 'react-router-dom';
|
||||
export { Input } from './ui/input';
|
||||
export { Skeleton } from './ui/skeleton';
|
||||
export { Toaster } from './ui/toaster';
|
||||
@@ -12,6 +13,11 @@ export { TitleAndLogo } from './TitleAndLogo';
|
||||
export { ThemeProvider } from './themeProvider';
|
||||
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||
export { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
|
||||
import TimeAgo from 'javascript-time-ago';
|
||||
import en from 'javascript-time-ago/locale/en.json';
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
|
||||
export function BioInput(props: { value?: string; onChange: (value: string) => void }) {
|
||||
return (
|
||||
@@ -23,7 +29,7 @@ export function BioInput(props: { value?: string; onChange: (value: string) => v
|
||||
props.onChange(e.target.value);
|
||||
}}
|
||||
placeholder="Add a bio..."
|
||||
className="w-full p-2 border rounded"
|
||||
className="w-full p-2 border rounded max-md:text-base"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -59,23 +65,35 @@ export function ChooseProfilePicInput(props: { onChange: (file: File) => void })
|
||||
);
|
||||
}
|
||||
|
||||
export function LargeProfilePicImg(props: { src?: string }) {
|
||||
return <img src={props.src} className="w-20 h-20 bg-neutral-200 rounded-full mr-2 object-cover" />;
|
||||
}
|
||||
|
||||
export function ProfilePicImg(props: { src?: string; smaller?: boolean }) {
|
||||
export function ProfilePicImg(props: { src?: string; size?: 'sm' | 'xxl'; linkTo?: string; initial?: string }) {
|
||||
return (
|
||||
<img
|
||||
src={props.src}
|
||||
className={'bg-neutral-200 rounded-full mr-2 object-cover shrink-0' + (props.smaller ? ' w-8 h-8' : ' w-10 h-10')}
|
||||
/>
|
||||
<Link to={props.linkTo || ''}>
|
||||
{props.src ? (
|
||||
<img
|
||||
src={props.src}
|
||||
className={
|
||||
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0' +
|
||||
(props.size === 'sm' ? ' w-8 h-8' : props.size === 'xxl' ? ' w-20 h-20' : ' w-10 h-10')
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
'bg-neutral-200 rounded-full mr-2 object-cover shrink-0 flex items-center justify-center text-neutral-700 ' +
|
||||
(props.size === 'sm'
|
||||
? ' w-8 h-8 text-[1.5rem]'
|
||||
: props.size === 'xxl'
|
||||
? ' w-20 h-20 text-[3.75rem]'
|
||||
: ' w-10 h-10 text-[1.875rem]')
|
||||
}
|
||||
>
|
||||
<div className="-mt-[8%]">{props.initial}</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubtleProfileID(props: { children: React.ReactNode }) {
|
||||
return <div className="ml-2 text-neutral-300 text-xs">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function SubtleRelativeTimeAgo(props: { dateTime?: Date }) {
|
||||
return (
|
||||
<div className="ml-auto text-neutral-300 text-xs whitespace-nowrap">
|
||||
@@ -88,12 +106,8 @@ export function TwitImg(props: { src?: string }) {
|
||||
return <img src={props.src} className="h-40 rounded object-cover" />;
|
||||
}
|
||||
|
||||
export function ReactionsAndReplyContainer(props: { children: React.ReactNode }) {
|
||||
return <div className="flex flex-col mt-2">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function ReactionsContainer(props: { children: React.ReactNode }) {
|
||||
return <div className="flex gap-4">{props.children}</div>;
|
||||
return <div className="flex gap-4 mt-2">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function RepliesContainer(props: { children: React.ReactNode }) {
|
||||
@@ -140,7 +154,7 @@ export function TwitTextInput(props: { onSubmit: (text: string) => void; submitB
|
||||
name="twitText"
|
||||
placeholder="What's happenin'"
|
||||
autoComplete="off"
|
||||
className="p-2 border rounded grow"
|
||||
className="p-2 border rounded grow max-md:text-base"
|
||||
/>
|
||||
<Button asChild>
|
||||
<input type="submit" value={props.submitButtonLabel} />
|
||||
@@ -167,3 +181,48 @@ export function AddTwitPicsInput(props: { onChange: (files: File[]) => void }) {
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwitWithRepliesContainer(props: { children: React.ReactNode; isTopLevel?: boolean }) {
|
||||
return (
|
||||
<div className={'py-2 flex flex-col items-stretch' + (props.isTopLevel ? ' border-t' : ' ml-14')}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwitContainer(props: { children: React.ReactNode }) {
|
||||
return <div className="flex gap-2">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function TwitBody(props: { children: React.ReactNode }) {
|
||||
return <div className="grow flex flex-col items-stretch">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function TwitHeader(props: { children: React.ReactNode }) {
|
||||
return <div className="flex items-baseline">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function TwitImgGallery(props: { children: React.ReactNode }) {
|
||||
return <div className="flex gap-2 mt-2 max-w-full overflow-auto">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function TwitText(props: { children: React.ReactNode; style?: React.CSSProperties }) {
|
||||
return <div style={props.style}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function QuoteContainer(props: { children: React.ReactNode }) {
|
||||
return <div className="border rounded">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function MainH1(props: { children: React.ReactNode }) {
|
||||
return <h1 className="text-2xl mb-4">{props.children}</h1>;
|
||||
}
|
||||
|
||||
export function SmallInlineButton(props: { children: React.ReactNode } & ButtonProps) {
|
||||
const {children, ...rest} = props
|
||||
return (
|
||||
<Button variant={'ghost'} className="h-6 px-1 -mx-1" {...rest}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
29
examples/twit/src/basicComponents/ui/popover.tsx
Normal file
29
examples/twit/src/basicComponents/ui/popover.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
@@ -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.0",
|
||||
"version": "0.4.5",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
@@ -16,8 +16,8 @@
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.0",
|
||||
"cojson-storage-sqlite": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"cojson-storage-sqlite": "^0.4.5",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"scripts": {
|
||||
"test": "vitest --browser chrome",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "cojson",
|
||||
"module": "dist/index.js",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.5",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -197,7 +197,7 @@ export class CoListView<
|
||||
}
|
||||
|
||||
/** @category 6. Meta */
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export class CoMapView<
|
||||
}
|
||||
|
||||
/** @category 6. Meta */
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { Group } from "./group.js";
|
||||
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
||||
import { AccountID } from "./account.js";
|
||||
import { AccountID, isAccountID } from "./account.js";
|
||||
import { parseJSON } from "../jsonStringify.js";
|
||||
|
||||
export type BinaryStreamInfo = {
|
||||
@@ -59,7 +59,7 @@ export class CoStreamView<
|
||||
this.fillFromCoValue();
|
||||
}
|
||||
|
||||
get meta(): Meta {
|
||||
get headerMeta(): Meta {
|
||||
return this.core.header.meta as Meta;
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ export class CoStreamView<
|
||||
return Object.keys(this.items) as SessionID[];
|
||||
}
|
||||
|
||||
accounts(): Set<AccountID | AgentID> {
|
||||
return new Set(this.sessions().map(accountOrAgentIDfromSessionID));
|
||||
accounts(): Set<AccountID> {
|
||||
return new Set(this.sessions().map(accountOrAgentIDfromSessionID).filter(isAccountID));
|
||||
}
|
||||
|
||||
nthItemIn(
|
||||
@@ -290,8 +290,6 @@ export class BinaryCoStreamView<
|
||||
extends CoStreamView<BinaryStreamItem, Meta>
|
||||
implements CoValue
|
||||
{
|
||||
id!: CoID<this>;
|
||||
|
||||
getBinaryChunks(
|
||||
allowUnfinished?: boolean
|
||||
):
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,155 +0,0 @@
|
||||
import { JsonValue } from "../jsonValue.js";
|
||||
import { CoStream, MutableCoStream } from "../coValues/coStream.js";
|
||||
import { CoValueCore } from "../coValueCore.js";
|
||||
import { Group } from "../coValues/group.js";
|
||||
import { AccountID, isAccountID } from "../coValues/account.js";
|
||||
import { CoID, CoValue } from "../coValue.js";
|
||||
import { SessionID, TransactionID } from "../ids.js";
|
||||
import { ValueOrSubQueried, QueryContext } from "../queries.js";
|
||||
import { QueriedAccount } from "./queriedAccount.js";
|
||||
|
||||
export type QueriedCoStreamItems<Item extends JsonValue | CoValue> = {
|
||||
last?: ValueOrSubQueried<Item>;
|
||||
by?: QueriedAccount;
|
||||
tx?: TransactionID;
|
||||
at?: Date;
|
||||
all: {
|
||||
value: ValueOrSubQueried<Item>;
|
||||
by?: QueriedAccount;
|
||||
tx: TransactionID;
|
||||
at: Date;
|
||||
}[];
|
||||
};
|
||||
|
||||
export class QueriedCoStream<S extends CoStream> {
|
||||
coStream!: S;
|
||||
id: CoID<S>;
|
||||
type = "costream" as const;
|
||||
|
||||
/** @internal */
|
||||
constructor(coStream: S, queryContext: QueryContext) {
|
||||
Object.defineProperty(this, "coStream", {
|
||||
get() {
|
||||
return coStream;
|
||||
},
|
||||
});
|
||||
this.id = coStream.id;
|
||||
|
||||
this.perSession = Object.fromEntries(
|
||||
coStream.sessions().map((sessionID) => {
|
||||
const items = [...coStream.itemsIn(sessionID)].map((item) =>
|
||||
queryContext.defineSubqueryPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
},
|
||||
value: {
|
||||
value: item.value as S["_item"],
|
||||
enumerable: true,
|
||||
},
|
||||
},
|
||||
[coStream.id]
|
||||
)
|
||||
);
|
||||
|
||||
const lastItem = items[items.length - 1];
|
||||
|
||||
return [
|
||||
sessionID,
|
||||
{
|
||||
get last() {
|
||||
return lastItem?.value;
|
||||
},
|
||||
get by() {
|
||||
return lastItem?.by;
|
||||
},
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies QueriedCoStreamItems<S["_item"]>,
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
this.perAccount = Object.fromEntries(
|
||||
[...coStream.accounts()].map((accountID) => {
|
||||
const items = [...coStream.itemsBy(accountID)].map((item) => queryContext.defineSubqueryPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
},
|
||||
value: {
|
||||
value: item.value as S["_item"],
|
||||
enumerable: true,
|
||||
},
|
||||
},
|
||||
[coStream.id]
|
||||
));
|
||||
|
||||
const lastItem = items[items.length - 1];
|
||||
|
||||
return [
|
||||
accountID,
|
||||
{
|
||||
get last() {
|
||||
return lastItem?.value;
|
||||
},
|
||||
get by() {
|
||||
return lastItem?.by;
|
||||
},
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies QueriedCoStreamItems<S["_item"]>,
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
this.me = isAccountID(queryContext.node.account.id)
|
||||
? this.perAccount[queryContext.node.account.id]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get meta(): S["meta"] {
|
||||
return this.coStream.meta;
|
||||
}
|
||||
|
||||
get group(): Group {
|
||||
return this.coStream.group;
|
||||
}
|
||||
|
||||
get core(): CoValueCore {
|
||||
return this.coStream.core;
|
||||
}
|
||||
|
||||
me?: QueriedCoStreamItems<S["_item"]>;
|
||||
perAccount: {
|
||||
[account: AccountID]: QueriedCoStreamItems<S["_item"]>;
|
||||
};
|
||||
perSession: {
|
||||
[session: SessionID]: QueriedCoStreamItems<S["_item"]>;
|
||||
};
|
||||
|
||||
push(item: S["_item"], privacy?: "private" | "trusting"): S {
|
||||
return this.coStream.push(item, privacy);
|
||||
}
|
||||
mutate(
|
||||
mutator: (mutable: MutableCoStream<S["_item"], S["meta"]>) => void
|
||||
): S {
|
||||
return this.coStream.mutate(mutator);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
@@ -1,337 +0,0 @@
|
||||
import {
|
||||
BinaryCoStream,
|
||||
CoList,
|
||||
CoMap,
|
||||
CoStream,
|
||||
Group,
|
||||
LocalNode,
|
||||
cojsonReady,
|
||||
} from "..";
|
||||
|
||||
beforeEach(async () => {
|
||||
await cojsonReady;
|
||||
});
|
||||
|
||||
test("Queries with maps work", async () => {
|
||||
const { node, accountID } =
|
||||
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let map = group.createMap<
|
||||
CoMap<{
|
||||
hello: "world";
|
||||
subMap: CoMap<{
|
||||
hello: "world" | "moon" | "sun";
|
||||
id: string;
|
||||
}>["id"];
|
||||
}>
|
||||
>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(map.id, (queriedMap) => {
|
||||
// console.log("update", update);
|
||||
if (queriedMap) {
|
||||
expect(queriedMap.type).toBe("comap");
|
||||
expect(queriedMap.id).toEqual(map.id);
|
||||
expect(queriedMap.core).toEqual(map.core);
|
||||
expect(queriedMap.group).toBeInstanceOf(Group);
|
||||
expect(queriedMap.group.id).toBe(group.id);
|
||||
expect(queriedMap.meta).toBe(null);
|
||||
expect(queriedMap.hello).toBe("world");
|
||||
expect(Object.keys(queriedMap)).toEqual(["hello", "subMap"]);
|
||||
if (queriedMap.edits.hello?.by?.profile?.name) {
|
||||
expect(queriedMap.edits.hello.by.id).toEqual(accountID);
|
||||
expect(queriedMap.edits.hello.by.profile.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(queriedMap.edits.hello.by.profile.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(queriedMap.edits.hello.by.isMe).toBe(true);
|
||||
expect(queriedMap.edits.hello.tx).toEqual(
|
||||
map.lastEditAt("hello")!.tx
|
||||
);
|
||||
expect(queriedMap.edits.hello.at).toEqual(
|
||||
new Date(map.lastEditAt("hello")!.at)
|
||||
);
|
||||
if (queriedMap.subMap) {
|
||||
expect(queriedMap.subMap.type).toBe("comap");
|
||||
expect(queriedMap.subMap.id).toEqual("foreignID");
|
||||
expect(queriedMap.subMap.core).toEqual(subMap.core);
|
||||
expect(queriedMap.subMap.group).toBeInstanceOf(Group);
|
||||
expect(queriedMap.subMap.group.id).toBe(group.id);
|
||||
expect(queriedMap.subMap.meta).toBe(null);
|
||||
if (queriedMap.subMap.hello === "moon") {
|
||||
// console.log("got to 'moon'");
|
||||
queriedMap.subMap.set("hello", "sun");
|
||||
} else if (
|
||||
queriedMap.subMap.hello === "sun" &&
|
||||
queriedMap.subMap.edits.hello?.by?.profile?.name ===
|
||||
"Hermes Puggington"
|
||||
) {
|
||||
// console.log("final update", queriedMap);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
map = map.set("hello", "world");
|
||||
|
||||
let subMap = group.createMap<
|
||||
CoMap<{
|
||||
hello: "world" | "moon" | "sun";
|
||||
id: string;
|
||||
}>
|
||||
>();
|
||||
|
||||
map = map.set("subMap", subMap.id);
|
||||
|
||||
subMap = subMap.mutate((subMap) => {
|
||||
subMap.set("hello", "world");
|
||||
subMap.set("id", "foreignID");
|
||||
});
|
||||
|
||||
subMap = subMap.set("hello", "moon");
|
||||
|
||||
await done;
|
||||
});
|
||||
|
||||
test("Queries with lists work", () => {
|
||||
const { node, accountID } =
|
||||
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(list.id, (queriedList) => {
|
||||
if (queriedList) {
|
||||
// console.log("update", queriedList, queriedList.edits);
|
||||
expect(queriedList.type).toBe("colist");
|
||||
expect(queriedList.id).toEqual(list.id);
|
||||
expect(queriedList.core).toEqual(list.core);
|
||||
expect(queriedList.group).toBeInstanceOf(Group);
|
||||
expect(queriedList.group.id).toBe(group.id);
|
||||
expect(queriedList.meta).toBe(null);
|
||||
expect(queriedList[0]).toBe("hello");
|
||||
expect(queriedList[1]).toBe("world");
|
||||
expect(queriedList[2]).toBe("moon");
|
||||
if (queriedList.edits[2]?.by?.profile?.name) {
|
||||
expect(queriedList.edits[2].by.id).toEqual(accountID);
|
||||
expect(queriedList.edits[2].by.profile.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(queriedList.edits[2].by.profile.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(queriedList.edits[2].by.isMe).toBe(true);
|
||||
expect(queriedList.edits[2].at).toBeInstanceOf(Date);
|
||||
if (queriedList.length === 3) {
|
||||
queriedList.append("sun");
|
||||
} else if (
|
||||
queriedList.length === 4 &&
|
||||
queriedList.edits[3]?.by?.profile?.name ===
|
||||
"Hermes Puggington"
|
||||
) {
|
||||
expect(queriedList[3]).toBe("sun");
|
||||
// console.log("final update", queriedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.mutate((list) => {
|
||||
list.append("hello");
|
||||
list.append("world");
|
||||
list.append("moon");
|
||||
});
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("List of nested maps works", () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<CoMap<{ hello: "world" }>["id"]>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(list.id, (queriedList) => {
|
||||
if (queriedList && queriedList[0]) {
|
||||
// console.log("update", queriedList);
|
||||
expect(queriedList[0]).toMatchObject({
|
||||
hello: "world",
|
||||
id: list.get(0)!,
|
||||
});
|
||||
// console.log("final update", queriedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.append(
|
||||
group.createMap<CoMap<{ hello: "world" }>>({
|
||||
hello: "world",
|
||||
}).id
|
||||
);
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("Can call .map on a quieried coList", async () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(list.id, (queriedList) => {
|
||||
if (queriedList && queriedList[0]) {
|
||||
// console.log("update", queriedList);
|
||||
expect(queriedList.map((item) => item + "!!!")).toEqual([
|
||||
"hello!!!",
|
||||
"world!!!",
|
||||
]);
|
||||
// console.log("final update", queriedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.mutate((list) => {
|
||||
list.append("hello");
|
||||
list.append("world");
|
||||
});
|
||||
|
||||
await done;
|
||||
});
|
||||
|
||||
test("Queries with streams work", () => {
|
||||
const { node, accountID } =
|
||||
LocalNode.withNewlyCreatedAccount({name: "Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let stream = group.createStream<CoStream<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(stream.id, (queriedStream) => {
|
||||
if (queriedStream) {
|
||||
// console.log("update", queriedStream);
|
||||
if (queriedStream.me?.by?.profile?.name) {
|
||||
expect(queriedStream.type).toBe("costream");
|
||||
expect(queriedStream.id).toEqual(stream.id);
|
||||
expect(queriedStream.core).toEqual(stream.core);
|
||||
expect(queriedStream.group).toBeInstanceOf(Group);
|
||||
expect(queriedStream.group.id).toBe(group.id);
|
||||
expect(queriedStream.meta).toBe(null);
|
||||
|
||||
expect(queriedStream.perSession[node.currentSessionID].last).toEqual(
|
||||
"world"
|
||||
);
|
||||
expect(queriedStream.perSession[node.currentSessionID].all[0].value).toEqual("hello");
|
||||
expect(queriedStream.perSession[node.currentSessionID].all[0].at).toEqual(new Date(
|
||||
stream.items[
|
||||
node.currentSessionID
|
||||
][0].madeAt
|
||||
));
|
||||
expect(queriedStream.perSession[node.currentSessionID].all[1].value).toEqual("world");
|
||||
expect(queriedStream.perSession[node.currentSessionID].all[1].at).toEqual(new Date(
|
||||
stream.items[
|
||||
node.currentSessionID
|
||||
][1].madeAt
|
||||
));
|
||||
expect(queriedStream.perSession[node.currentSessionID].by?.id).toEqual(accountID);
|
||||
expect(queriedStream.perSession[node.currentSessionID].by?.profile?.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(queriedStream.perSession[node.currentSessionID].by?.profile?.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(queriedStream.perSession[node.currentSessionID].by?.isMe).toBe(true);
|
||||
expect(queriedStream.perSession[node.currentSessionID].at).toBeInstanceOf(Date);
|
||||
|
||||
expect(queriedStream.perAccount[accountID].last).toEqual(
|
||||
"world"
|
||||
);
|
||||
expect(queriedStream.perAccount[accountID].all[0].value).toEqual("hello");
|
||||
expect(queriedStream.perAccount[accountID].all[0].at).toEqual(new Date(
|
||||
stream.items[
|
||||
node.currentSessionID
|
||||
][0].madeAt
|
||||
));
|
||||
expect(queriedStream.perAccount[accountID].all[1].value).toEqual("world");
|
||||
expect(queriedStream.perAccount[accountID].all[1].at).toEqual(new Date(
|
||||
stream.items[
|
||||
node.currentSessionID
|
||||
][1].madeAt
|
||||
));
|
||||
expect(queriedStream.perAccount[accountID].by?.id).toEqual(accountID);
|
||||
expect(queriedStream.perAccount[accountID].by?.profile?.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(queriedStream.perAccount[accountID].by?.profile?.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(queriedStream.perAccount[accountID].by?.isMe).toBe(true);
|
||||
expect(queriedStream.perAccount[accountID].at).toBeInstanceOf(Date);
|
||||
|
||||
expect(queriedStream.me).toEqual(queriedStream.perAccount[accountID]);
|
||||
// console.log("final update", queriedStream);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
stream = stream.push("hello");
|
||||
stream = stream.push("world");
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("Streams of nested maps work", () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({name:"Hermes Puggington"});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let stream =
|
||||
group.createStream<CoStream<CoMap<{ hello: "world" }>["id"]>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = node.query(stream.id, (queriedStream) => {
|
||||
if (queriedStream && queriedStream.me?.last) {
|
||||
// console.log("update", queriedList);
|
||||
expect(queriedStream.me.last).toMatchObject({
|
||||
hello: "world",
|
||||
id: map.id,
|
||||
});
|
||||
// console.log("final update", queriedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const map = group.createMap<CoMap<{ hello: "world" }>>({
|
||||
hello: "world",
|
||||
});
|
||||
|
||||
stream = stream.push(map.id);
|
||||
|
||||
return done;
|
||||
});
|
||||
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.5",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"transform": {
|
||||
"\\.[jt]sx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"useESM": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"(.+)\\.js": "$1"
|
||||
},
|
||||
"extensionsToTreatAsEsm": [
|
||||
".ts"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/dist/"
|
||||
]
|
||||
}
|
||||
}
|
||||
391
packages/jazz-autosub/src/autoSub.test.ts
Normal file
391
packages/jazz-autosub/src/autoSub.test.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { CoList, CoMap, CoStream, Group, LocalNode, cojsonReady } from "cojson";
|
||||
import { autoSub } from ".";
|
||||
|
||||
beforeEach(async () => {
|
||||
await cojsonReady;
|
||||
});
|
||||
|
||||
test("Queries with maps work", async () => {
|
||||
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let map = group.createMap<
|
||||
CoMap<{
|
||||
hello: "world";
|
||||
subMap: CoMap<{
|
||||
hello: "world" | "moon" | "sun";
|
||||
id: string;
|
||||
}>["id"];
|
||||
}>
|
||||
>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(map.id, node, (resolvedMap) => {
|
||||
// console.log("update", update);
|
||||
if (resolvedMap) {
|
||||
expect(resolvedMap.coValueType).toBe("comap");
|
||||
expect(resolvedMap.id).toEqual(map.id);
|
||||
expect(resolvedMap.meta.group).toBeInstanceOf(Group);
|
||||
expect(resolvedMap.meta.group.id).toBe(group.id);
|
||||
expect(resolvedMap.meta.headerMeta).toBe(null);
|
||||
expect(resolvedMap.hello).toBe("world");
|
||||
expect(Object.keys(resolvedMap)).toEqual(["hello", "subMap"]);
|
||||
if (resolvedMap.meta.edits.hello?.by?.profile?.name) {
|
||||
expect(resolvedMap.meta.edits.hello.by.id).toEqual(
|
||||
accountID
|
||||
);
|
||||
expect(resolvedMap.meta.edits.hello.by.profile.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(
|
||||
resolvedMap.meta.edits.hello.by.profile.name
|
||||
).toEqual("Hermes Puggington");
|
||||
expect(resolvedMap.meta.edits.hello.by.isMe).toBe(true);
|
||||
expect(resolvedMap.meta.edits.hello.tx).toEqual(
|
||||
map.lastEditAt("hello")!.tx
|
||||
);
|
||||
expect(resolvedMap.meta.edits.hello.at).toEqual(
|
||||
new Date(map.lastEditAt("hello")!.at)
|
||||
);
|
||||
if (resolvedMap.subMap) {
|
||||
expect(resolvedMap.subMap.coValueType).toBe("comap");
|
||||
expect(resolvedMap.subMap.id).toEqual("foreignID");
|
||||
expect(resolvedMap.subMap.meta.group).toBeInstanceOf(
|
||||
Group
|
||||
);
|
||||
expect(resolvedMap.subMap.meta.group.id).toBe(group.id);
|
||||
expect(resolvedMap.subMap.meta.headerMeta).toBe(null);
|
||||
if (resolvedMap.subMap.hello === "moon") {
|
||||
// console.log("got to 'moon'");
|
||||
resolvedMap.subMap.set("hello", "sun");
|
||||
} else if (
|
||||
resolvedMap.subMap.hello === "sun" &&
|
||||
resolvedMap.subMap.meta.edits.hello?.by?.profile
|
||||
?.name === "Hermes Puggington"
|
||||
) {
|
||||
// console.log("final update", resolvedMap);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
map = map.set("hello", "world");
|
||||
|
||||
let subMap = group.createMap<
|
||||
CoMap<{
|
||||
hello: "world" | "moon" | "sun";
|
||||
id: string;
|
||||
}>
|
||||
>();
|
||||
|
||||
map = map.set("subMap", subMap.id);
|
||||
|
||||
subMap = subMap.mutate((subMap) => {
|
||||
subMap.set("hello", "world");
|
||||
subMap.set("id", "foreignID");
|
||||
});
|
||||
|
||||
subMap = subMap.set("hello", "moon");
|
||||
|
||||
await done;
|
||||
});
|
||||
|
||||
test("Queries with lists work", () => {
|
||||
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
|
||||
if (resolvedList) {
|
||||
// console.log("update", resolvedList, resolvedList.meta.edits);
|
||||
expect(resolvedList.coValueType).toBe("colist");
|
||||
expect(resolvedList.id).toEqual(list.id);
|
||||
expect(resolvedList.meta.group).toBeInstanceOf(Group);
|
||||
expect(resolvedList.meta.group.id).toBe(group.id);
|
||||
expect(resolvedList.meta.headerMeta).toBe(null);
|
||||
expect(resolvedList[0]).toBe("hello");
|
||||
expect(resolvedList[1]).toBe("world");
|
||||
expect(resolvedList[2]).toBe("moon");
|
||||
if (resolvedList.meta.edits[2]?.by?.profile?.name) {
|
||||
expect(resolvedList.meta.edits[2].by.id).toEqual(accountID);
|
||||
expect(resolvedList.meta.edits[2].by.profile.id).toEqual(
|
||||
node.expectProfileLoaded(accountID).id
|
||||
);
|
||||
expect(resolvedList.meta.edits[2].by.profile.name).toEqual(
|
||||
"Hermes Puggington"
|
||||
);
|
||||
expect(resolvedList.meta.edits[2].by.isMe).toBe(true);
|
||||
expect(resolvedList.meta.edits[2].at).toBeInstanceOf(Date);
|
||||
if (resolvedList.length === 3) {
|
||||
resolvedList.append("sun");
|
||||
} else if (
|
||||
resolvedList.length === 4 &&
|
||||
resolvedList.meta.edits[3]?.by?.profile?.name ===
|
||||
"Hermes Puggington"
|
||||
) {
|
||||
expect(resolvedList[3]).toBe("sun");
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.mutate((list) => {
|
||||
list.append("hello");
|
||||
list.append("world");
|
||||
list.append("moon");
|
||||
});
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("List of nested maps works", () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<CoMap<{ hello: "world" }>["id"]>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
|
||||
if (resolvedList && resolvedList[0]) {
|
||||
// console.log("update", resolvedList);
|
||||
expect(resolvedList[0]).toMatchObject({
|
||||
hello: "world",
|
||||
id: list.get(0)!,
|
||||
});
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.append(
|
||||
group.createMap<CoMap<{ hello: "world" }>>({
|
||||
hello: "world",
|
||||
}).id
|
||||
);
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("Can call .map on a quieried coList", async () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let list = group.createList<CoList<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(list.id, node, (resolvedList) => {
|
||||
if (resolvedList && resolvedList[0]) {
|
||||
// console.log("update", resolvedList);
|
||||
expect(resolvedList.map((item) => item + "!!!")).toEqual([
|
||||
"hello!!!",
|
||||
"world!!!",
|
||||
]);
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list = list.mutate((list) => {
|
||||
list.append("hello");
|
||||
list.append("world");
|
||||
});
|
||||
|
||||
await done;
|
||||
});
|
||||
|
||||
test("Queries with streams work", () => {
|
||||
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let stream = group.createStream<CoStream<string>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(stream.id, node, (resolvedStream) => {
|
||||
if (resolvedStream) {
|
||||
// console.log("update", resolvedStream);
|
||||
if (resolvedStream.me?.by?.profile?.name) {
|
||||
expect(resolvedStream.coValueType).toBe("costream");
|
||||
expect(resolvedStream.id).toEqual(stream.id);
|
||||
expect(resolvedStream.meta.group).toBeInstanceOf(Group);
|
||||
expect(resolvedStream.meta.group.id).toBe(group.id);
|
||||
expect(resolvedStream.meta.headerMeta).toBe(null);
|
||||
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].last
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[0].value
|
||||
).toEqual("hello");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[0].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][0].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[1].value
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].all[1].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][1].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.id
|
||||
).toEqual(accountID);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.profile?.id
|
||||
).toEqual(node.expectProfileLoaded(accountID).id);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.profile?.name
|
||||
).toEqual("Hermes Puggington");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].by?.isMe
|
||||
).toBe(true);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perSession)[
|
||||
node.currentSessionID
|
||||
].at
|
||||
).toBeInstanceOf(Date);
|
||||
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.last
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[0].value
|
||||
).toEqual("hello");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[0].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][0].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[1].value
|
||||
).toEqual("world");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.all[1].at
|
||||
).toEqual(
|
||||
new Date(stream.items[node.currentSessionID][1].madeAt)
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.id
|
||||
).toEqual(accountID);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.profile?.id
|
||||
).toEqual(node.expectProfileLoaded(accountID).id);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.profile?.name
|
||||
).toEqual("Hermes Puggington");
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.by?.isMe
|
||||
).toBe(true);
|
||||
expect(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
.at
|
||||
).toBeInstanceOf(Date);
|
||||
|
||||
expect(resolvedStream.me).toEqual(
|
||||
Object.fromEntries(resolvedStream.perAccount)[accountID]
|
||||
);
|
||||
// console.log("final update", resolvedStream);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
stream = stream.push("hello");
|
||||
stream = stream.push("world");
|
||||
|
||||
return done;
|
||||
});
|
||||
|
||||
test("Streams of nested maps work", () => {
|
||||
const { node } = LocalNode.withNewlyCreatedAccount({
|
||||
name: "Hermes Puggington",
|
||||
});
|
||||
|
||||
const group = node.createGroup();
|
||||
|
||||
let stream =
|
||||
group.createStream<CoStream<CoMap<{ hello: "world" }>["id"]>>();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const unsubQuery = autoSub(stream.id, node, (resolvedStream) => {
|
||||
if (resolvedStream && resolvedStream.me?.last) {
|
||||
// console.log("update", resolvedList);
|
||||
expect(resolvedStream.me.last).toMatchObject({
|
||||
hello: "world",
|
||||
id: map.id,
|
||||
});
|
||||
// console.log("final update", resolvedList);
|
||||
resolve();
|
||||
unsubQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const map = group.createMap<CoMap<{ hello: "world" }>>({
|
||||
hello: "world",
|
||||
});
|
||||
|
||||
stream = stream.push(map.id);
|
||||
|
||||
return done;
|
||||
});
|
||||
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;
|
||||
}
|
||||
146
packages/jazz-autosub/src/resolvedCoValues/resolvedCoStream.ts
Normal file
146
packages/jazz-autosub/src/resolvedCoValues/resolvedCoStream.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
CoValue,
|
||||
JsonValue,
|
||||
CojsonInternalTypes,
|
||||
CoStream,
|
||||
CoID,
|
||||
cojsonInternals,
|
||||
Group,
|
||||
AccountID,
|
||||
SessionID,
|
||||
MutableCoStream,
|
||||
} from "cojson";
|
||||
import { ValueOrResolvedRef, AutoSubContext } from "../autoSub.js";
|
||||
import { ResolvedAccount } from "./resolvedAccount.js";
|
||||
|
||||
export type ResolvedCoStreamEntry<Item extends JsonValue | CoValue> = {
|
||||
last?: ValueOrResolvedRef<Item>;
|
||||
by?: ResolvedAccount;
|
||||
tx?: CojsonInternalTypes.TransactionID;
|
||||
at?: Date;
|
||||
all: {
|
||||
value: ValueOrResolvedRef<Item>;
|
||||
by?: ResolvedAccount;
|
||||
tx: CojsonInternalTypes.TransactionID;
|
||||
at: Date;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ResolvedCoStreamMeta<S extends CoStream> = {
|
||||
coValue: S;
|
||||
headerMeta: S["headerMeta"];
|
||||
group: Group;
|
||||
}
|
||||
|
||||
export class ResolvedCoStream<S extends CoStream> {
|
||||
id: CoID<S>;
|
||||
coValueType = "costream" as const;
|
||||
meta: ResolvedCoStreamMeta<S>;
|
||||
me?: ResolvedCoStreamEntry<S["_item"]>;
|
||||
perAccount: [account: AccountID, items: ResolvedCoStreamEntry<S["_item"]>][];
|
||||
perSession: [session: SessionID, items: ResolvedCoStreamEntry<S["_item"]>][];
|
||||
|
||||
/** @internal */
|
||||
constructor(coStream: S, autoSubContext: AutoSubContext) {
|
||||
this.id = coStream.id;
|
||||
this.meta = {
|
||||
coValue: coStream,
|
||||
headerMeta: coStream.headerMeta,
|
||||
group: coStream.group,
|
||||
}
|
||||
|
||||
this.perSession = coStream.sessions().map((sessionID) => {
|
||||
const items = [...coStream.itemsIn(sessionID)].map((item) =>
|
||||
autoSubContext.defineResolvedRefPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: cojsonInternals.isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
},
|
||||
value: {
|
||||
value: item.value as S["_item"],
|
||||
enumerable: true,
|
||||
},
|
||||
},
|
||||
[coStream.id]
|
||||
)
|
||||
);
|
||||
|
||||
const lastItem = items[items.length - 1];
|
||||
|
||||
return [
|
||||
sessionID,
|
||||
{
|
||||
get last() {
|
||||
return lastItem?.value;
|
||||
},
|
||||
get by() {
|
||||
return lastItem?.by;
|
||||
},
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies ResolvedCoStreamEntry<S["_item"]>,
|
||||
];
|
||||
});
|
||||
|
||||
this.perAccount = [...coStream.accounts()].map((accountID) => {
|
||||
const items = [...coStream.itemsBy(accountID)].map((item) =>
|
||||
autoSubContext.defineResolvedRefPropertiesIn(
|
||||
{
|
||||
tx: item.tx,
|
||||
at: new Date(item.at),
|
||||
},
|
||||
{
|
||||
by: {
|
||||
value: cojsonInternals.isAccountID(item.by)
|
||||
? item.by
|
||||
: (undefined as never),
|
||||
enumerable: true,
|
||||
},
|
||||
value: {
|
||||
value: item.value as S["_item"],
|
||||
enumerable: true,
|
||||
},
|
||||
},
|
||||
[coStream.id]
|
||||
)
|
||||
);
|
||||
|
||||
const lastItem = items[items.length - 1];
|
||||
|
||||
const entry = {
|
||||
get last() {
|
||||
return lastItem?.value;
|
||||
},
|
||||
get by() {
|
||||
return lastItem?.by;
|
||||
},
|
||||
tx: lastItem?.tx,
|
||||
at: lastItem?.at,
|
||||
all: items,
|
||||
} satisfies ResolvedCoStreamEntry<S["_item"]>;
|
||||
|
||||
if (accountID === autoSubContext.node.account.id) {
|
||||
this.me = entry;
|
||||
}
|
||||
|
||||
return [accountID, entry];
|
||||
});
|
||||
}
|
||||
|
||||
push(item: S["_item"], privacy?: "private" | "trusting"): S {
|
||||
return this.meta.coValue.push(item, privacy);
|
||||
}
|
||||
mutate(
|
||||
mutator: (mutable: MutableCoStream<S["_item"], S["headerMeta"]>) => void
|
||||
): S {
|
||||
return this.meta.coValue.mutate(mutator);
|
||||
}
|
||||
}
|
||||
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.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.4.0",
|
||||
"jazz-browser": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "^0.4.0",
|
||||
"jazz-autosub": "^0.4.5",
|
||||
"jazz-browser": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.0",
|
||||
"cojson-storage-indexeddb": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"cojson-storage-indexeddb": "^0.4.5",
|
||||
"jazz-autosub": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
|
||||
@@ -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.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.4.0",
|
||||
"jazz-react": "^0.4.0",
|
||||
"jazz-browser-auth-local": "^0.4.5",
|
||||
"jazz-react": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.tsx",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
|
||||
@@ -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.0",
|
||||
"version": "0.4.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.4.0",
|
||||
"jazz-browser": "^0.4.0",
|
||||
"cojson": "^0.4.5",
|
||||
"jazz-browser": "^0.4.5",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.tsx",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import {
|
||||
LocalNode,
|
||||
CoID,
|
||||
Queried,
|
||||
CoValue,
|
||||
BinaryCoStream,
|
||||
QueriedAccount,
|
||||
Account,
|
||||
AccountMeta,
|
||||
AccountMigration,
|
||||
Profile,
|
||||
CoMap,
|
||||
} from "cojson";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
AuthProvider,
|
||||
consumeInviteLinkFromWindowLocation,
|
||||
createBrowserNode,
|
||||
readBlobFromBinaryStream,
|
||||
} from "jazz-browser";
|
||||
import { readBlobFromBinaryStream } from "jazz-browser";
|
||||
import { Profile } from "cojson";
|
||||
import { CoMap } from "cojson";
|
||||
|
||||
import { Resolved, ResolvedAccount, autoSub } from "jazz-autosub";
|
||||
export type { Resolved, ResolvedCoMap } from "jazz-autosub";
|
||||
export {
|
||||
ResolvedAccount,
|
||||
ResolvedCoList,
|
||||
ResolvedCoMapBase,
|
||||
ResolvedCoStream,
|
||||
ResolvedGroup,
|
||||
} from "jazz-autosub";
|
||||
|
||||
const JazzContext = React.createContext<
|
||||
| {
|
||||
me: Queried<Account>;
|
||||
me: Resolved<Account>;
|
||||
localNode: LocalNode;
|
||||
logOut: () => void;
|
||||
}
|
||||
@@ -100,7 +108,7 @@ export function WithJazz(props: {
|
||||
};
|
||||
}, [auth, syncAddress]);
|
||||
|
||||
const me = useSyncedQueryWithNode("me", node) as QueriedAccount | undefined;
|
||||
const me = useAutoSubWithNode("me", node) as ResolvedAccount | undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -122,8 +130,9 @@ export function WithJazz(props: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that exposes the Jazz context provided by `<WithJazz/>`, most importantly the `LocalNode`
|
||||
* for the logged in user (which you can use to create `Group`s, and `CoValue`s in those).
|
||||
* Hook that exposes the Jazz context provided by `<WithJazz/>`, most importantly `me`, the account of
|
||||
* the current in user (which you can use access the account's `root` or `profile`,
|
||||
* and to create `Group`s as the current user, in which you can then create `CoValue`s).
|
||||
*
|
||||
* Also provides a `logOut` function, which invokes the log-out logic of the Auth Provider passed to `<WithJazz/>`.
|
||||
*/
|
||||
@@ -139,7 +148,7 @@ export function useJazz<
|
||||
}
|
||||
|
||||
return {
|
||||
me: context.me as QueriedAccount<Account<P, R, Meta>>,
|
||||
me: context.me as ResolvedAccount<Account<P, R, Meta>>,
|
||||
localNode: context.localNode,
|
||||
logOut: context.logOut,
|
||||
};
|
||||
@@ -148,36 +157,44 @@ export function useJazz<
|
||||
/**
|
||||
* Hook that subscribes to all updates of a given `CoValue` (identified by its `CoID`) and that automatically resolves references to nested `CoValue`s, loading and subscribing to them as well.
|
||||
*
|
||||
* See `Queried<T>` in `cojson` to see which fields and methods are available on the returned object.
|
||||
* See `Resolved<T>` in `jazz-autosub` to see which fields and methods are available on the returned object.
|
||||
*
|
||||
* @param id The `CoID` of the `CoValue` to subscribe to. Can be undefined (in which case the hook returns undefined).
|
||||
*/
|
||||
export function useSyncedQuery<
|
||||
|
||||
export function useAutoSub<T extends CoValue>(
|
||||
id?: CoID<T>
|
||||
): Resolved<T> | undefined;
|
||||
|
||||
/**
|
||||
* Hook that subscribes to all updates the current user account and that automatically resolves references to nested `CoValue`s, loading and subscribing to them as well.
|
||||
*
|
||||
* See `Resolved<T>` in `jazz-autosub` to see which fields and methods are available on the returned object.
|
||||
*
|
||||
*/
|
||||
export function useAutoSub<
|
||||
P extends Profile = Profile,
|
||||
R extends CoMap = CoMap,
|
||||
Meta extends AccountMeta = AccountMeta
|
||||
>(id: "me"): QueriedAccount<Account<P, R, Meta>> | undefined;
|
||||
export function useSyncedQuery<T extends CoValue>(
|
||||
id?: CoID<T>
|
||||
): Queried<T> | undefined;
|
||||
export function useSyncedQuery(
|
||||
>(id: "me"): ResolvedAccount<Account<P, R, Meta>> | undefined;
|
||||
export function useAutoSub(
|
||||
id?: CoID<CoValue> | "me"
|
||||
): Queried<CoValue> | QueriedAccount | undefined {
|
||||
return useSyncedQueryWithNode(id, useJazz().localNode);
|
||||
): Resolved<CoValue> | ResolvedAccount | undefined {
|
||||
return useAutoSubWithNode(id, useJazz().localNode);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function useSyncedQueryWithNode(
|
||||
function useAutoSubWithNode(
|
||||
id?: CoID<CoValue> | "me",
|
||||
localNode?: LocalNode
|
||||
): Queried<CoValue> | QueriedAccount | undefined {
|
||||
): Resolved<CoValue> | ResolvedAccount | undefined {
|
||||
const [result, setResult] = useState<
|
||||
Queried<CoValue> | QueriedAccount | undefined
|
||||
Resolved<CoValue> | ResolvedAccount | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !localNode) return;
|
||||
const unsubscribe = localNode.query(id, setResult);
|
||||
const unsubscribe = autoSub(id, localNode, setResult);
|
||||
return unsubscribe;
|
||||
}, [id, localNode]);
|
||||
|
||||
|
||||
208
yarn.lock
208
yarn.lock
@@ -473,6 +473,33 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d"
|
||||
integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==
|
||||
|
||||
"@floating-ui/core@^1.4.2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
|
||||
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.1.3"
|
||||
|
||||
"@floating-ui/dom@^1.5.1":
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
|
||||
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.4.2"
|
||||
"@floating-ui/utils" "^0.1.3"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20"
|
||||
integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.5.1"
|
||||
|
||||
"@floating-ui/utils@^0.1.3":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.4.tgz#19654d1026cc410975d46445180e70a5089b3e7d"
|
||||
integrity sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.10":
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
||||
@@ -1175,6 +1202,14 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-arrow@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
|
||||
integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
|
||||
@@ -1227,6 +1262,82 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
|
||||
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
|
||||
integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-focus-scope@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
|
||||
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
|
||||
"@radix-ui/react-id@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
|
||||
integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
||||
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||
"@radix-ui/react-focus-guards" "1.0.1"
|
||||
"@radix-ui/react-focus-scope" "1.0.4"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-popper" "1.1.3"
|
||||
"@radix-ui/react-portal" "1.0.4"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-popper@1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
||||
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@radix-ui/react-arrow" "1.0.3"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
"@radix-ui/react-use-rect" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/rect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-portal@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
|
||||
@@ -1235,6 +1346,14 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-portal@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
|
||||
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-presence@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
|
||||
@@ -1316,6 +1435,14 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
|
||||
integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/rect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-use-size@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
|
||||
@@ -1332,6 +1459,13 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
|
||||
integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@remix-run/router@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
|
||||
@@ -2159,6 +2293,13 @@ argparse@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-hidden@^1.1.1:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
||||
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
aria-query@^5.0.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
@@ -3152,6 +3293,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
detect-node-es@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
||||
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
|
||||
|
||||
devtools-protocol@0.0.1147663:
|
||||
version "0.0.1147663"
|
||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz#4ec5610b39a6250d1f87e6b9c7e16688ed0ac78e"
|
||||
@@ -4022,6 +4168,11 @@ get-func-name@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
||||
integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
|
||||
|
||||
get-nonce@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
|
||||
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
|
||||
|
||||
get-package-type@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
|
||||
@@ -4561,6 +4712,13 @@ inquirer@^8.2.4:
|
||||
through "^2.3.6"
|
||||
wrap-ansi "^6.0.1"
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ip@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
|
||||
@@ -5548,7 +5706,7 @@ loglevel@^1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
|
||||
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==
|
||||
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -7007,6 +7165,25 @@ react-is@^18.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-remove-scroll-bar@^2.3.3:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9"
|
||||
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
|
||||
dependencies:
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-remove-scroll@2.5.5:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
|
||||
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
|
||||
dependencies:
|
||||
react-remove-scroll-bar "^2.3.3"
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.1.0"
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-router-dom@^6.16.0:
|
||||
version "6.16.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.16.0.tgz#86f24658da35eb66727e75ecbb1a029e33ee39d9"
|
||||
@@ -7022,6 +7199,15 @@ react-router@6.16.0, react-router@^6.16.0:
|
||||
dependencies:
|
||||
"@remix-run/router" "1.9.0"
|
||||
|
||||
react-style-singleton@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
|
||||
dependencies:
|
||||
get-nonce "^1.0.0"
|
||||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-time-ago@^7.2.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-time-ago/-/react-time-ago-7.2.1.tgz#101a549a8d7f51c225c82c74abc517ecef647b24"
|
||||
@@ -8021,6 +8207,11 @@ tsconfig-paths@^4.1.2:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.0.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
|
||||
@@ -8207,6 +8398,21 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-callback-ref@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
|
||||
integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sidecar@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
||||
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
|
||||
dependencies:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
Reference in New Issue
Block a user