Compare commits

..

7 Commits

Author SHA1 Message Date
Anselm
10b372da6e Publish
- jazz-example-todo@0.0.12
 - cojson@0.1.0
 - jazz-browser@0.1.0
 - jazz-browser-auth-local@0.1.0
 - jazz-react@0.1.0
 - jazz-react-auth-local@0.1.0
 - jazz-storage-indexeddb@0.1.0
2023-08-19 16:32:32 +01:00
Anselm
b556a36db3 Rename "Team" to "Group" 2023-08-19 16:31:40 +01:00
Anselm
4d71ab8aac Lint 2023-08-19 16:25:15 +01:00
Anselm
f1297c613b Polishing 2023-08-19 16:22:43 +01:00
Anselm
1ee5c2b3c8 Cleaner invite links 2023-08-19 15:58:55 +01:00
Anselm
f14337f862 Publish
- jazz-example-todo@0.0.11
 - cojson@0.0.24
 - jazz-browser@0.0.7
 - jazz-browser-auth-local@0.0.7
 - jazz-react@0.0.17
 - jazz-react-auth-local@0.0.14
 - jazz-storage-indexeddb@0.0.11
2023-08-19 15:48:33 +01:00
Anselm
b8f4571474 Fix first time invite flow 2023-08-19 15:48:19 +01:00
29 changed files with 630 additions and 600 deletions

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Jazz Todo List Example</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.10",
"version": "0.0.12",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,13 +15,14 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "^0.0.16",
"jazz-react-auth-local": "^0.0.13",
"jazz-react": "^0.1.0",
"jazz-react-auth-local": "^0.1.0",
"lucide-react": "^0.265.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.6"
"tailwindcss-animate": "^1.0.6",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -20,6 +20,7 @@ import { SubmittableInput } from "./components/SubmittableInput";
import { createInviteLink } from "jazz-react";
import { useToast } from "./components/ui/use-toast";
import { Skeleton } from "./components/ui/skeleton";
import uniqolor from "uniqolor";
type TaskContent = { done: boolean; text: string };
type Task = CoMap<TaskContent>;
@@ -38,8 +39,8 @@ function App() {
const createList = useCallback(
(title: string) => {
const listTeam = localNode.createTeam();
const list = listTeam.createMap<TodoListContent>();
const listGroup = localNode.createGroup();
const list = listGroup.createMap<TodoListContent>();
list.edit((list) => {
list.set("title", title);
@@ -72,7 +73,7 @@ function App() {
}, [localNode]);
return (
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 md:pt-[30vh] pb-10 px-5">
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
{listId ? (
<TodoList listId={listId} />
) : (
@@ -100,24 +101,18 @@ export function TodoList({ listId }: { listId: CoID<TodoList> }) {
const createTask = (text: string) => {
if (!list) return;
let task = list.coValue.getTeam().createMap<TaskContent>();
const task = list.coValue.getGroup().createMap<TaskContent>();
task = task.edit((task) => {
task.edit((task) => {
task.set("text", text);
task.set("done", false);
});
console.log("Created task", task.id, task.toJSON());
const listAfter = list.edit((list) => {
list.edit((list) => {
list.set(task.id, true);
});
console.log("Updated list", listAfter.toJSON());
};
const { toast } = useToast();
return (
<div className="max-w-full w-4xl">
<div className="flex justify-between items-center gap-4 mb-4">
@@ -131,25 +126,7 @@ export function TodoList({ listId }: { listId: CoID<TodoList> }) {
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
)}
</h1>
{list && list.coValue.getTeam().myRole() === "admin" && <Button
size="sm"
className="py-0"
disabled={!list}
variant="outline"
onClick={() => {
if (list) {
const inviteLink = createInviteLink(list, "writer");
navigator.clipboard.writeText(inviteLink).then(() =>
toast({
description:
"Copied invite link to clipboard!",
})
);
}
}}
>
Invite
</Button>}
{list && <InviteButton list={list} />}
</div>
<Table>
<TableHeader>
@@ -218,11 +195,56 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
function NameBadge({ accountID }: { accountID?: AccountID }) {
const profile = useProfile({ accountID });
const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
const brightColor = uniqolor(accountID || "", { lightness: 80 }).color;
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
return (
<span className="rounded-full bg-neutral-200 dark:bg-neutral-600 py-0.5 px-2 text-xs text-neutral-500 dark:text-neutral-300">
<span
className="rounded-full py-0.5 px-2 text-xs"
style={{
color: theme == "light" ? darkColor : brightColor,
background: theme == "light" ? brightColor : darkColor,
}}
>
{profile?.get("name") || "..."}
</span>
);
}
function InviteButton({ list }: { list: TodoList }) {
const [existingInviteLink, setExistingInviteLink] = useState<string>();
const { toast } = useToast();
return (
list.coValue.getGroup().myRole() === "admin" && (
<Button
size="sm"
className="py-0"
disabled={!list}
variant="outline"
onClick={() => {
let inviteLink = existingInviteLink;
if (list && !inviteLink) {
inviteLink = createInviteLink(list, "writer");
setExistingInviteLink(inviteLink);
}
if (inviteLink) {
navigator.clipboard.writeText(inviteLink).then(() =>
toast({
description: "Copied invite link to clipboard!",
})
);
}
}}
>
Invite
</Button>
)
);
}
export default App;

View File

@@ -11,9 +11,10 @@ import { Toaster } from "./components/ui/toaster.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider>
<div className="flex items-center gap-2 justify-center mt-5"><img src="jazz-logo.png" className="h-5"/> Jazz Todo List Example</div>
<WithJazz
auth={LocalAuth({
appName: "Todo List Example",
appName: "Jazz Todo List Example",
Component: PrettyAuthComponent,
})}
syncAddress={

View File

@@ -27,7 +27,7 @@ THIS IS WORK IN PROGRESS
### `Collaborative` Values
- CoMap (`string``Immutable`, last-writer-wins per key)
- Team (`AgentID``Role`)
- Group (`AgentID``Role`)
- CoList (`Immutable[]`, addressable positions, insertAfter semantics)
- Agent (`{signerID, sealerID}[]`)
- CoStream (independent per-session streams of `Immutable`s)

View File

@@ -5,7 +5,7 @@
"types": "dist/index.d.ts",
"type": "module",
"license": "MIT",
"version": "0.0.23",
"version": "0.1.0",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",

View File

@@ -19,14 +19,14 @@ test("Can create a node while creating a new account with profile", async () =>
);
});
test("A node with an account can create teams and and objects within them", async () => {
test("A node with an account can create groups and and objects within them", async () => {
const { node, accountID } =
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
const team = await node.createTeam();
expect(team).not.toBeNull();
const group = await node.createGroup();
expect(group).not.toBeNull();
let map = team.createMap();
let map = group.createMap();
map = map.edit((edit) => {
edit.set("foo", "bar", "private");
expect(edit.get("foo")).toEqual("bar");
@@ -41,10 +41,10 @@ test("Can create account with one node, and then load it on another", async () =
const { node, accountID, accountSecret } =
LocalNode.withNewlyCreatedAccount("Hermes Puggington");
const team = await node.createTeam();
expect(team).not.toBeNull();
const group = await node.createGroup();
expect(group).not.toBeNull();
let map = team.createMap();
let map = group.createMap();
map = map.edit((edit) => {
edit.set("foo", "bar", "private");
expect(edit.get("foo")).toEqual("bar");

View File

@@ -14,7 +14,7 @@ import {
} from "./crypto.js";
import { AgentID } from "./ids.js";
import { CoMap, LocalNode } from "./index.js";
import { Team, TeamContent } from "./team.js";
import { Group, GroupContent } from "./group.js";
export function accountHeaderForInitialAgentSecret(
agentSecret: AgentSecret
@@ -22,7 +22,7 @@ export function accountHeaderForInitialAgentSecret(
const agent = getAgentID(agentSecret);
return {
type: "comap",
ruleset: { type: "team", initialAdmin: agent },
ruleset: { type: "group", initialAdmin: agent },
meta: {
type: "account",
},
@@ -31,13 +31,13 @@ export function accountHeaderForInitialAgentSecret(
};
}
export class Account extends Team {
export class Account extends Group {
get id(): AccountID {
return this.teamMap.id as AccountID;
return this.groupMap.id as AccountID;
}
getCurrentAgentID(): AgentID {
const agents = this.teamMap
const agents = this.groupMap
.keys()
.filter((k): k is AgentID => k.startsWith("sealer_"));
@@ -70,10 +70,10 @@ export class ControlledAccount
constructor(
agentSecret: AgentSecret,
teamMap: CoMap<AccountContent, AccountMeta>,
groupMap: CoMap<AccountContent, AccountMeta>,
node: LocalNode
) {
super(teamMap, node);
super(groupMap, node);
this.agentSecret = agentSecret;
}
@@ -133,7 +133,7 @@ export class AnonymousControlledAccount
}
}
export type AccountContent = TeamContent & { profile: CoID<Profile> };
export type AccountContent = GroupContent & { profile: CoID<Profile> };
export type AccountMeta = { type: "account" };
export type AccountID = CoID<CoMap<AccountContent, AccountMeta>>;

View File

@@ -125,17 +125,17 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
).toBe(false);
});
test("New transactions in a team correctly update owned values, including subscriptions", async () => {
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
const [account, sessionID] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(account, sessionID);
const team = node.createTeam();
const group = node.createGroup();
const timeBeforeEdit = Date.now();
await new Promise((resolve) => setTimeout(resolve, 10));
let map = team.createMap();
let map = group.createMap();
let mapAfterEdit = map.edit((map) => {
map.set("hello", "world");
@@ -159,7 +159,7 @@ test("New transactions in a team correctly update owned values, including subscr
]
} satisfies Transaction;
const { expectedNewHash } = team.teamMap.coValue.expectedNewHashAfter(sessionID, [
const { expectedNewHash } = group.groupMap.coValue.expectedNewHashAfter(sessionID, [
resignationThatWeJustLearnedAbout,
]);
@@ -170,7 +170,7 @@ test("New transactions in a team correctly update owned values, including subscr
expect(map.coValue.getValidSortedTransactions().length).toBe(1);
const manuallyAdddedTxSuccess = team.teamMap.coValue.tryAddTransactions(node.ownSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
const manuallyAdddedTxSuccess = group.groupMap.coValue.tryAddTransactions(node.ownSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
expect(manuallyAdddedTxSuccess).toBe(true);

View File

@@ -27,7 +27,7 @@ import {
determineValidTransactions,
isKeyForKeyField,
} from "./permissions.js";
import { Team, expectTeamContent } from "./team.js";
import { Group, expectGroupContent } from "./group.js";
import { LocalNode } from "./node.js";
import { CoValueKnownState, NewContentMessage } from "./sync.js";
import { RawCoID, SessionID, TransactionID } from "./ids.js";
@@ -106,10 +106,10 @@ export class CoValue {
this._sessions = internalInitSessions;
this.node = node;
if (header.ruleset.type == "ownedByTeam") {
if (header.ruleset.type == "ownedByGroup") {
this.node
.expectCoValueLoaded(header.ruleset.team)
.subscribe((_teamUpdate) => {
.expectCoValueLoaded(header.ruleset.group)
.subscribe((_groupUpdate) => {
this._cachedContent = undefined;
const newContent = this.getCurrentContent();
for (const listener of this.listeners) {
@@ -385,8 +385,8 @@ export class CoValue {
}
getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
if (this.header.ruleset.type === "team") {
const content = expectTeamContent(this.getCurrentContent());
if (this.header.ruleset.type === "group") {
const content = expectGroupContent(this.getCurrentContent());
const currentKeyId = content.get("readKey");
@@ -400,20 +400,20 @@ export class CoValue {
secret: secret,
id: currentKeyId,
};
} else if (this.header.ruleset.type === "ownedByTeam") {
} else if (this.header.ruleset.type === "ownedByGroup") {
return this.node
.expectCoValueLoaded(this.header.ruleset.team)
.expectCoValueLoaded(this.header.ruleset.group)
.getCurrentReadKey();
} else {
throw new Error(
"Only teams or values owned by teams have read secrets"
"Only groups or values owned by groups have read secrets"
);
}
}
getReadKey(keyID: KeyID): KeySecret | undefined {
if (this.header.ruleset.type === "team") {
const content = expectTeamContent(this.getCurrentContent());
if (this.header.ruleset.type === "group") {
const content = expectGroupContent(this.getCurrentContent());
// Try to find key revelation for us
@@ -477,26 +477,26 @@ export class CoValue {
}
return undefined;
} else if (this.header.ruleset.type === "ownedByTeam") {
} else if (this.header.ruleset.type === "ownedByGroup") {
return this.node
.expectCoValueLoaded(this.header.ruleset.team)
.expectCoValueLoaded(this.header.ruleset.group)
.getReadKey(keyID);
} else {
throw new Error(
"Only teams or values owned by teams have read secrets"
"Only groups or values owned by groups have read secrets"
);
}
}
getTeam(): Team {
if (this.header.ruleset.type !== "ownedByTeam") {
throw new Error("Only values owned by teams have teams");
getGroup(): Group {
if (this.header.ruleset.type !== "ownedByGroup") {
throw new Error("Only values owned by groups have groups");
}
return new Team(
expectTeamContent(
return new Group(
expectGroupContent(
this.node
.expectCoValueLoaded(this.header.ruleset.team)
.expectCoValueLoaded(this.header.ruleset.group)
.getCurrentContent()
),
this.node
@@ -553,12 +553,12 @@ export class CoValue {
}
getDependedOnCoValues(): RawCoID[] {
return this.header.ruleset.type === "team"
? expectTeamContent(this.getCurrentContent())
return this.header.ruleset.type === "group"
? expectGroupContent(this.getCurrentContent())
.keys()
.filter((k): k is AccountID => k.startsWith("co_"))
: this.header.ruleset.type === "ownedByTeam"
? [this.header.ruleset.team]
: this.header.ruleset.type === "ownedByGroup"
? [this.header.ruleset.group]
: [];
}
}

View File

@@ -25,7 +25,7 @@ import {
import { Role } from "./permissions.js";
import { base58 } from "@scure/base";
export type TeamContent = {
export type GroupContent = {
profile: CoID<Profile> | null;
[key: AccountIDOrAgentID]: Role;
readKey: KeyID;
@@ -36,34 +36,34 @@ export type TeamContent = {
>;
};
export function expectTeamContent(
export function expectGroupContent(
content: ContentType
): CoMap<TeamContent, JsonObject | null> {
): CoMap<GroupContent, JsonObject | null> {
if (content.type !== "comap") {
throw new Error("Expected map");
}
return content as CoMap<TeamContent, JsonObject | null>;
return content as CoMap<GroupContent, JsonObject | null>;
}
export class Team {
teamMap: CoMap<TeamContent, JsonObject | null>;
export class Group {
groupMap: CoMap<GroupContent, JsonObject | null>;
node: LocalNode;
constructor(
teamMap: CoMap<TeamContent, JsonObject | null>,
groupMap: CoMap<GroupContent, JsonObject | null>,
node: LocalNode
) {
this.teamMap = teamMap;
this.groupMap = groupMap;
this.node = node;
}
get id(): CoID<CoMap<TeamContent, JsonObject | null>> {
return this.teamMap.id;
get id(): CoID<CoMap<GroupContent, JsonObject | null>> {
return this.groupMap.id;
}
roleOf(accountID: AccountIDOrAgentID): Role | undefined {
return this.teamMap.get(accountID);
return this.groupMap.get(accountID);
}
myRole(): Role | undefined {
@@ -71,8 +71,8 @@ export class Team {
}
addMember(accountID: AccountIDOrAgentID, role: Role) {
this.teamMap = this.teamMap.edit((map) => {
const currentReadKey = this.teamMap.coValue.getCurrentReadKey();
this.groupMap = this.groupMap.edit((map) => {
const currentReadKey = this.groupMap.coValue.getCurrentReadKey();
if (!currentReadKey.secret) {
throw new Error("Can't add member without read key secret");
@@ -80,7 +80,7 @@ export class Team {
const agent = this.node.resolveAccountAgent(
accountID,
"Expected to know agent to add them to team"
"Expected to know agent to add them to group"
);
map.set(accountID, role, "trusting");
@@ -93,11 +93,11 @@ export class Team {
`${currentReadKey.id}_for_${accountID}`,
seal(
currentReadKey.secret,
this.teamMap.coValue.node.account.currentSealerSecret(),
this.groupMap.coValue.node.account.currentSealerSecret(),
getAgentSealerID(agent),
{
in: this.teamMap.coValue.id,
tx: this.teamMap.coValue.nextTransactionID(),
in: this.groupMap.coValue.id,
tx: this.groupMap.coValue.nextTransactionID(),
}
),
"trusting"
@@ -117,9 +117,9 @@ export class Team {
}
rotateReadKey() {
const currentlyPermittedReaders = this.teamMap.keys().filter((key) => {
const currentlyPermittedReaders = this.groupMap.keys().filter((key) => {
if (key.startsWith("co_") || isAgentID(key)) {
const role = this.teamMap.get(key);
const role = this.groupMap.get(key);
return (
role === "admin" || role === "writer" || role === "reader"
);
@@ -128,7 +128,7 @@ export class Team {
}
}) as AccountIDOrAgentID[];
const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey();
const maybeCurrentReadKey = this.groupMap.coValue.getCurrentReadKey();
if (!maybeCurrentReadKey.secret) {
throw new Error(
@@ -143,7 +143,7 @@ export class Team {
const newReadKey = newRandomKeySecret();
this.teamMap = this.teamMap.edit((map) => {
this.groupMap = this.groupMap.edit((map) => {
for (const readerID of currentlyPermittedReaders) {
const reader = this.node.resolveAccountAgent(
readerID,
@@ -154,11 +154,11 @@ export class Team {
`${newReadKey.id}_for_${readerID}`,
seal(
newReadKey.secret,
this.teamMap.coValue.node.account.currentSealerSecret(),
this.groupMap.coValue.node.account.currentSealerSecret(),
getAgentSealerID(reader),
{
in: this.teamMap.coValue.id,
tx: this.teamMap.coValue.nextTransactionID(),
in: this.groupMap.coValue.id,
tx: this.groupMap.coValue.nextTransactionID(),
}
),
"trusting"
@@ -179,7 +179,7 @@ export class Team {
}
removeMember(accountID: AccountIDOrAgentID) {
this.teamMap = this.teamMap.edit((map) => {
this.groupMap = this.groupMap.edit((map) => {
map.set(accountID, "revoked", "trusting");
});
@@ -194,8 +194,8 @@ export class Team {
.createCoValue({
type: "comap",
ruleset: {
type: "ownedByTeam",
team: this.teamMap.id,
type: "ownedByGroup",
group: this.groupMap.id,
},
meta: meta || null,
...createdNowUnique(),
@@ -206,10 +206,10 @@ export class Team {
testWithDifferentAccount(
account: GeneralizedControlledAccount,
sessionId: SessionID
): Team {
return new Team(
expectTeamContent(
this.teamMap.coValue
): Group {
return new Group(
expectGroupContent(
this.groupMap.coValue
.testWithDifferentAccount(account, sessionId)
.getCurrentContent()
),

View File

@@ -14,7 +14,7 @@ import {
import { connectedPeers } from "./streamUtils.js";
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
import { Team, expectTeamContent } from "./team.js"
import { Group, expectGroupContent } from "./group.js"
import type { SessionID, AgentID } from "./ids.js";
import type { CoID, ContentType } from "./contentType.js";
@@ -27,7 +27,7 @@ import type {
ProfileContent,
Profile,
} from "./account.js";
import type { InviteSecret } from "./team.js";
import type { InviteSecret } from "./group.js";
type Value = JsonValue | ContentType;
@@ -44,7 +44,7 @@ export const cojsonInternals = {
agentSecretFromSecretSeed,
secretSeedLength,
shortHashLength,
expectTeamContent
expectGroupContent
};
export {
@@ -53,7 +53,7 @@ export {
CoMap,
AnonymousControlledAccount,
ControlledAccount,
Team
Group
};
export type {

View File

@@ -12,11 +12,11 @@ import {
import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js";
import {
InviteSecret,
Team,
TeamContent,
expectTeamContent,
Group,
GroupContent,
expectGroupContent,
secretSeedFromInviteSecret,
} from "./team.js";
} from "./group.js";
import { Peer, SyncManager } from "./sync.js";
import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
import { CoID, ContentType } from "./contentType.js";
@@ -151,23 +151,23 @@ export class LocalNode {
}
async acceptInvite<T extends ContentType>(
teamOrOwnedValueID: CoID<T>,
groupOrOwnedValueID: CoID<T>,
inviteSecret: InviteSecret
): Promise<void> {
const teamOrOwnedValue = await this.load(teamOrOwnedValueID);
const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
if (teamOrOwnedValue.coValue.header.ruleset.type === "ownedByTeam") {
if (groupOrOwnedValue.coValue.header.ruleset.type === "ownedByGroup") {
return this.acceptInvite(
teamOrOwnedValue.coValue.header.ruleset.team as CoID<
CoMap<TeamContent>
groupOrOwnedValue.coValue.header.ruleset.group as CoID<
CoMap<GroupContent>
>,
inviteSecret
);
} else if (teamOrOwnedValue.coValue.header.ruleset.type !== "team") {
throw new Error("Can only accept invites to teams");
} else if (groupOrOwnedValue.coValue.header.ruleset.type !== "group") {
throw new Error("Can only accept invites to groups");
}
const team = new Team(expectTeamContent(teamOrOwnedValue), this);
const group = new Group(expectGroupContent(groupOrOwnedValue), this);
const inviteAgentSecret = agentSecretFromSecretSeed(
secretSeedFromInviteSecret(inviteSecret)
@@ -175,8 +175,8 @@ export class LocalNode {
const inviteAgentID = getAgentID(inviteAgentSecret);
const invitationRole = await new Promise((resolve, reject) => {
team.teamMap.subscribe((teamMap) => {
const role = teamMap.get(inviteAgentID);
group.groupMap.subscribe((groupMap) => {
const role = groupMap.get(inviteAgentID);
if (role) {
resolve(role);
}
@@ -194,7 +194,7 @@ export class LocalNode {
throw new Error("No invitation found");
}
const existingRole = team.teamMap.get(this.account.id);
const existingRole = group.groupMap.get(this.account.id);
if (
existingRole === "admin" ||
@@ -204,12 +204,12 @@ export class LocalNode {
return;
}
const teamAsInvite = team.testWithDifferentAccount(
const groupAsInvite = group.testWithDifferentAccount(
new AnonymousControlledAccount(inviteAgentSecret),
newRandomSessionID(inviteAgentID)
);
teamAsInvite.addMember(
groupAsInvite.addMember(
this.account.id,
invitationRole === "adminInvite"
? "admin"
@@ -218,8 +218,12 @@ export class LocalNode {
: "reader"
);
team.teamMap.coValue._sessions = teamAsInvite.teamMap.coValue.sessions;
team.teamMap.coValue._cachedContent = undefined;
group.groupMap.coValue._sessions = groupAsInvite.groupMap.coValue.sessions;
group.groupMap.coValue._cachedContent = undefined;
for (const groupListener of group.groupMap.coValue.listeners) {
groupListener(group.groupMap.coValue.getCurrentContent());
}
}
expectCoValueLoaded(id: RawCoID, expectation?: string): CoValue {
@@ -241,7 +245,7 @@ export class LocalNode {
expectProfileLoaded(id: AccountID, expectation?: string): Profile {
const account = this.expectCoValueLoaded(id, expectation);
const profileID = expectTeamContent(account.getCurrentContent()).get(
const profileID = expectGroupContent(account.getCurrentContent()).get(
"profile"
);
if (!profileID) {
@@ -268,12 +272,12 @@ export class LocalNode {
newRandomSessionID(getAgentID(agentSecret))
);
const accountAsTeam = new Team(
expectTeamContent(account.getCurrentContent()),
const accountAsGroup = new Group(
expectGroupContent(account.getCurrentContent()),
account.node
);
accountAsTeam.teamMap.edit((editable) => {
accountAsGroup.groupMap.edit((editable) => {
editable.set(getAgentID(agentSecret), "admin", "trusting");
const readKey = newRandomKeySecret();
@@ -301,7 +305,7 @@ export class LocalNode {
account.node
);
const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({
const profile = accountAsGroup.createMap<ProfileContent, ProfileMeta>({
type: "profile",
});
@@ -309,13 +313,13 @@ export class LocalNode {
editable.set("name", name, "trusting");
});
accountAsTeam.teamMap.edit((editable) => {
accountAsGroup.groupMap.edit((editable) => {
editable.set("profile", profile.id, "trusting");
});
const accountOnThisNode = this.expectCoValueLoaded(account.id);
accountOnThisNode._sessions = {...accountAsTeam.teamMap.coValue.sessions};
accountOnThisNode._sessions = {...accountAsGroup.groupMap.coValue.sessions};
accountOnThisNode._cachedContent = undefined;
return controlledAccount;
@@ -330,7 +334,7 @@ export class LocalNode {
if (
coValue.header.type !== "comap" ||
coValue.header.ruleset.type !== "team" ||
coValue.header.ruleset.type !== "group" ||
!coValue.header.meta ||
!("type" in coValue.header.meta) ||
coValue.header.meta.type !== "account"
@@ -343,22 +347,22 @@ export class LocalNode {
}
return new Account(
coValue.getCurrentContent() as CoMap<TeamContent, AccountMeta>,
coValue.getCurrentContent() as CoMap<GroupContent, AccountMeta>,
this
).getCurrentAgentID();
}
createTeam(): Team {
const teamCoValue = this.createCoValue({
createGroup(): Group {
const groupCoValue = this.createCoValue({
type: "comap",
ruleset: { type: "team", initialAdmin: this.account.id },
ruleset: { type: "group", initialAdmin: this.account.id },
meta: null,
...createdNowUnique(),
});
let teamContent = expectTeamContent(teamCoValue.getCurrentContent());
let groupContent = expectGroupContent(groupCoValue.getCurrentContent());
teamContent = teamContent.edit((editable) => {
groupContent = groupContent.edit((editable) => {
editable.set(this.account.id, "admin", "trusting");
const readKey = newRandomKeySecret();
@@ -370,8 +374,8 @@ export class LocalNode {
this.account.currentSealerSecret(),
this.account.currentSealerID(),
{
in: teamCoValue.id,
tx: teamCoValue.nextTransactionID(),
in: groupCoValue.id,
tx: groupCoValue.nextTransactionID(),
}
),
"trusting"
@@ -380,7 +384,7 @@ export class LocalNode {
editable.set("readKey", readKey.id, "trusting");
});
return new Team(teamContent, this);
return new Group(groupContent, this);
}
testWithDifferentAccount(

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ import {
} from "./account.js";
export type PermissionsDef =
| { type: "team"; initialAdmin: AccountIDOrAgentID }
| { type: "ownedByTeam"; team: RawCoID }
| { type: "group"; initialAdmin: AccountIDOrAgentID }
| { type: "ownedByGroup"; group: RawCoID }
| { type: "unsafeAllowAll" };
export type Role =
@@ -33,7 +33,7 @@ export type Role =
export function determineValidTransactions(
coValue: CoValue
): { txID: TransactionID; tx: Transaction }[] {
if (coValue.header.ruleset.type === "team") {
if (coValue.header.ruleset.type === "group") {
const allTrustingTransactionsSorted = Object.entries(
coValue.sessions
).flatMap(([sessionID, sessionLog]) => {
@@ -43,7 +43,7 @@ export function determineValidTransactions(
if (tx.privacy === "trusting") {
return true;
} else {
console.warn("Unexpected private transaction in Team");
console.warn("Unexpected private transaction in Group");
return false;
}
}) as {
@@ -60,7 +60,7 @@ export function determineValidTransactions(
const initialAdmin = coValue.header.ruleset.initialAdmin;
if (!initialAdmin) {
throw new Error("Team must have initialAdmin");
throw new Error("Group must have initialAdmin");
}
const memberState: { [agent: AccountIDOrAgentID]: Role } = {};
@@ -81,12 +81,12 @@ export function determineValidTransactions(
| MapOpPayload<"readKey", JsonValue>
| MapOpPayload<"profile", CoID<Profile>>;
if (tx.changes.length !== 1) {
console.warn("Team transaction must have exactly one change");
console.warn("Group transaction must have exactly one change");
continue;
}
if (change.op !== "set") {
console.warn("Team transaction must set a role or readKey");
console.warn("Group transaction must set a role or readKey");
continue;
}
@@ -138,7 +138,7 @@ export function determineValidTransactions(
change.value !== "writerInvite" &&
change.value !== "readerInvite"
) {
console.warn("Team transaction must set a valid role");
console.warn("Group transaction must set a valid role");
continue;
}
@@ -176,7 +176,7 @@ export function determineValidTransactions(
}
} else {
console.warn(
"Team transaction must be made by current admin or invite"
"Group transaction must be made by current admin or invite"
);
continue;
}
@@ -189,16 +189,16 @@ export function determineValidTransactions(
}
return validTransactions;
} else if (coValue.header.ruleset.type === "ownedByTeam") {
const teamContent = coValue.node
} else if (coValue.header.ruleset.type === "ownedByGroup") {
const groupContent = coValue.node
.expectCoValueLoaded(
coValue.header.ruleset.team,
"Determining valid transaction in owned object but its team wasn't loaded"
coValue.header.ruleset.group,
"Determining valid transaction in owned object but its group wasn't loaded"
)
.getCurrentContent();
if (teamContent.type !== "comap") {
throw new Error("Team must be a map");
if (groupContent.type !== "comap") {
throw new Error("Group must be a map");
}
return Object.entries(coValue.sessions).flatMap(
@@ -208,7 +208,7 @@ export function determineValidTransactions(
);
return sessionLog.transactions
.filter((tx) => {
const transactorRoleAtTxTime = teamContent.getAtTime(
const transactorRoleAtTxTime = groupContent.getAtTime(
transactor,
tx.madeAt
);

View File

@@ -3,7 +3,7 @@ import { LocalNode } from "./node.js";
import { Peer, PeerID, SyncMessage } from "./sync.js";
import { expectMap } from "./contentType.js";
import { MapOpPayload } from "./contentTypes/coMap.js";
import { Team } from "./team.js";
import { Group } from "./group.js";
import {
ReadableStream,
WritableStream,
@@ -23,9 +23,9 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
@@ -53,7 +53,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownStateMsg = await reader.read();
expect(mapTellKnownStateMsg.value).toEqual({
@@ -62,7 +62,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const newContentMsg = await reader.read();
@@ -71,7 +71,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
id: map.coValue.id,
header: {
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
createdAt: map.coValue.header.createdAt,
uniqueness: map.coValue.header.uniqueness,
@@ -104,9 +104,9 @@ test("Node replies with only new tx to subscribe with some known state", async (
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
@@ -137,7 +137,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownStateMsg = await reader.read();
expect(mapTellKnownStateMsg.value).toEqual({
@@ -146,7 +146,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapNewContentMsg = await reader.read();
@@ -186,9 +186,9 @@ test("After subscribing, node sends own known state and new txs to peer", async
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -214,7 +214,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownStateMsg = await reader.read();
expect(mapTellKnownStateMsg.value).toEqual({
@@ -223,7 +223,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapNewContentHeaderOnlyMsg = await reader.read();
@@ -303,9 +303,9 @@ test("Client replies with known new content to tellKnownState from server", asyn
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
@@ -323,7 +323,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(teamStateEx(team));
// expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const writer = inTx.getWriter();
@@ -337,7 +337,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
});
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownStateMsg = await reader.read();
expect(mapTellKnownStateMsg.value).toEqual({
@@ -346,7 +346,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapNewContentMsg = await reader.read();
@@ -382,9 +382,9 @@ test("No matter the optimistic known state, node respects invalid known state me
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -410,7 +410,7 @@ test("No matter the optimistic known state, node respects invalid known state me
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownStateMsg = await reader.read();
expect(mapTellKnownStateMsg.value).toEqual({
@@ -419,7 +419,7 @@ test("No matter the optimistic known state, node respects invalid known state me
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapNewContentHeaderOnlyMsg = await reader.read();
@@ -485,9 +485,9 @@ test("If we add a peer, but it never subscribes to a coValue, it won't get any m
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, _inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -514,9 +514,9 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, _inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -535,7 +535,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
// });
expect((await reader.read()).value).toMatchObject({
action: "load",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
});
const mapSubscribeMsg = await reader.read();
@@ -552,7 +552,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
});
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapNewContentMsg = await reader.read();
@@ -588,7 +588,7 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const [inRx, _inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -607,10 +607,10 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
// });
expect((await reader.read()).value).toMatchObject({
action: "load",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
});
const map = team.createMap();
const map = group.createMap();
const mapSubscribeMsg = await reader.read();
@@ -620,7 +620,7 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(adminID));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapContentMsg = await reader.read();
@@ -640,9 +640,9 @@ test("When we connect a new server peer, we try to sync all existing coValues to
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, _inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -657,11 +657,11 @@ test("When we connect a new server peer, we try to sync all existing coValues to
const reader = outRx.getReader();
// const _adminSubscribeMessage = await reader.read();
const teamSubscribeMessage = await reader.read();
const groupSubscribeMessage = await reader.read();
expect(teamSubscribeMessage.value).toEqual({
expect(groupSubscribeMessage.value).toEqual({
action: "load",
...team.teamMap.coValue.knownState(),
...group.groupMap.coValue.knownState(),
} satisfies SyncMessage);
const secondMessage = await reader.read();
@@ -676,9 +676,9 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const map = team.createMap();
const map = group.createMap();
const [inRx, inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -704,7 +704,7 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe
const reader = outRx.getReader();
// expect((await reader.read()).value).toMatchObject(admStateEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamStateEx(team));
expect((await reader.read()).value).toMatchObject(groupStateEx(group));
const mapTellKnownState = await reader.read();
expect(mapTellKnownState.value).toEqual({
@@ -719,7 +719,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
const node1 = new LocalNode(admin, session);
const team = node1.createTeam();
const group = node1.createGroup();
const [inRx1, inTx1] = newStreamPair<SyncMessage>();
const [outRx1, outTx1] = newStreamPair<SyncMessage>();
@@ -754,40 +754,40 @@ test.skip("When replaying creation and transactions of a coValue as new content,
action: "load",
id: admin.id,
});
const teamSubscribeMsg = await from1.read();
expect(teamSubscribeMsg.value).toMatchObject({
const groupSubscribeMsg = await from1.read();
expect(groupSubscribeMsg.value).toMatchObject({
action: "load",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
});
await to2.write(adminSubscribeMessage.value!);
await to2.write(teamSubscribeMsg.value!);
await to2.write(groupSubscribeMsg.value!);
// const adminTellKnownStateMsg = await from2.read();
// expect(adminTellKnownStateMsg.value).toMatchObject(admStateEx(admin.id));
const teamTellKnownStateMsg = await from2.read();
expect(teamTellKnownStateMsg.value).toMatchObject(teamStateEx(team));
const groupTellKnownStateMsg = await from2.read();
expect(groupTellKnownStateMsg.value).toMatchObject(groupStateEx(group));
expect(
node2.sync.peers["test1"]!.optimisticKnownStates[
team.teamMap.coValue.id
group.groupMap.coValue.id
]
).toBeDefined();
// await to1.write(adminTellKnownStateMsg.value!);
await to1.write(teamTellKnownStateMsg.value!);
await to1.write(groupTellKnownStateMsg.value!);
// const adminContentMsg = await from1.read();
// expect(adminContentMsg.value).toMatchObject(admContEx(admin.id));
const teamContentMsg = await from1.read();
expect(teamContentMsg.value).toMatchObject(teamContentEx(team));
const groupContentMsg = await from1.read();
expect(groupContentMsg.value).toMatchObject(groupContentEx(group));
// await to2.write(adminContentMsg.value!);
await to2.write(teamContentMsg.value!);
await to2.write(groupContentMsg.value!);
const map = team.createMap();
const map = group.createMap();
const mapSubscriptionMsg = await from1.read();
expect(mapSubscriptionMsg.value).toMatchObject({
@@ -840,9 +840,9 @@ test.skip("When loading a coValue on one node, the server node it is requested f
const node1 = new LocalNode(admin, session);
const team = node1.createTeam();
const group = node1.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
});
@@ -868,9 +868,9 @@ test("Can sync a coValue through a server to another client", async () => {
const client1 = new LocalNode(admin, session);
const team = client1.createTeam();
const group = client1.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
});
@@ -910,9 +910,9 @@ test("Can sync a coValue with private transactions through a server to another c
const client1 = new LocalNode(admin, session);
const team = client1.createTeam();
const group = client1.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "private");
});
@@ -952,7 +952,7 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const [inRx, inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -971,10 +971,10 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
// });
expect((await reader.read()).value).toMatchObject({
action: "load",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
});
const map = team.createMap();
const map = group.createMap();
const mapSubscribeMsg = await reader.read();
@@ -984,7 +984,7 @@ test("When a peer's incoming/readable stream closes, we remove the peer", async
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapContentMsg = await reader.read();
@@ -1006,7 +1006,7 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session);
const team = node.createTeam();
const group = node.createGroup();
const [inRx, inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();
@@ -1025,10 +1025,10 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
// });
expect((await reader.read()).value).toMatchObject({
action: "load",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
});
const map = team.createMap();
const map = group.createMap();
const mapSubscribeMsg = await reader.read();
@@ -1038,7 +1038,7 @@ test("When a peer's outgoing/writable stream closes, we remove the peer", async
} satisfies SyncMessage);
// expect((await reader.read()).value).toMatchObject(admContEx(admin.id));
expect((await reader.read()).value).toMatchObject(teamContentEx(team));
expect((await reader.read()).value).toMatchObject(groupContentEx(group));
const mapContentMsg = await reader.read();
@@ -1066,9 +1066,9 @@ test("If we start loading a coValue before connecting to a peer that has it, it
const node1 = new LocalNode(admin, session);
const team = node1.createTeam();
const group = node1.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((editable) => {
editable.set("hello", "world", "trusting");
});
@@ -1096,10 +1096,10 @@ test("If we start loading a coValue before connecting to a peer that has it, it
);
});
function teamContentEx(team: Team) {
function groupContentEx(group: Group) {
return {
action: "content",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
};
}
@@ -1110,10 +1110,10 @@ function admContEx(adminID: AccountID) {
};
}
function teamStateEx(team: Team) {
function groupStateEx(group: Group) {
return {
action: "known",
id: team.teamMap.coValue.id,
id: group.groupMap.coValue.id,
};
}

View File

@@ -1,7 +1,7 @@
import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js";
import { newRandomSessionID } from "./coValue.js";
import { LocalNode } from "./node.js";
import { expectTeamContent } from "./team.js";
import { expectGroupContent } from "./group.js";
import { AnonymousControlledAccount } from "./account.js";
import { SessionID } from "./ids.js";
@@ -13,69 +13,69 @@ export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccoun
return [new AnonymousControlledAccount(agentSecret), sessionID];
}
export function newTeam() {
export function newGroup() {
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, sessionID);
const team = node.createCoValue({
const group = node.createCoValue({
type: "comap",
ruleset: { type: "team", initialAdmin: admin.id },
ruleset: { type: "group", initialAdmin: admin.id },
meta: null,
...createdNowUnique(),
});
const teamContent = expectTeamContent(team.getCurrentContent());
const groupContent = expectGroupContent(group.getCurrentContent());
teamContent.edit((editable) => {
groupContent.edit((editable) => {
editable.set(admin.id, "admin", "trusting");
expect(editable.get(admin.id)).toEqual("admin");
});
return { node, team, admin };
return { node, group, admin };
}
export function teamWithTwoAdmins() {
const { team, admin, node } = newTeam();
export function groupWithTwoAdmins() {
const { group, admin, node } = newGroup();
const otherAdmin = node.createAccount("otherAdmin");
let content = expectTeamContent(team.getCurrentContent());
let content = expectGroupContent(group.getCurrentContent());
content.edit((editable) => {
editable.set(otherAdmin.id, "admin", "trusting");
expect(editable.get(otherAdmin.id)).toEqual("admin");
});
content = expectTeamContent(team.getCurrentContent());
content = expectGroupContent(group.getCurrentContent());
if (content.type !== "comap") {
throw new Error("Expected map");
}
expect(content.get(otherAdmin.id)).toEqual("admin");
return { team, admin, otherAdmin, node };
return { group, admin, otherAdmin, node };
}
export function newTeamHighLevel() {
export function newGroupHighLevel() {
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, sessionID);
const team = node.createTeam();
const group = node.createGroup();
return { admin, node, team };
return { admin, node, group };
}
export function teamWithTwoAdminsHighLevel() {
const { admin, node, team } = newTeamHighLevel();
export function groupWithTwoAdminsHighLevel() {
const { admin, node, group } = newGroupHighLevel();
const otherAdmin = node.createAccount("otherAdmin");
team.addMember(otherAdmin.id, "admin");
group.addMember(otherAdmin.id, "admin");
return { admin, node, team, otherAdmin };
return { admin, node, group, otherAdmin };
}
export function shouldNotResolve<T>(

View File

@@ -1,11 +1,11 @@
{
"name": "jazz-browser-auth-local",
"version": "0.0.6",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"jazz-browser": "^0.0.6",
"jazz-browser": "^0.1.0",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-browser",
"version": "0.0.6",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.0.23",
"jazz-storage-indexeddb": "^0.0.10",
"cojson": "^0.1.0",
"jazz-storage-indexeddb": "^0.1.0",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -7,7 +7,7 @@ import {
SyncMessage,
Peer,
ContentType,
Team,
Group,
CoID,
} from "cojson";
import { ReadableStream, WritableStream } from "isomorphic-streams";
@@ -247,24 +247,24 @@ export function createInviteLink(
const node = coValue.node;
let currentCoValue = coValue;
while (currentCoValue.header.ruleset.type === "ownedByTeam") {
while (currentCoValue.header.ruleset.type === "ownedByGroup") {
currentCoValue = node.expectCoValueLoaded(
currentCoValue.header.ruleset.team
currentCoValue.header.ruleset.group
);
}
if (currentCoValue.header.ruleset.type !== "team") {
throw new Error("Can't create invite link for object without team");
if (currentCoValue.header.ruleset.type !== "group") {
throw new Error("Can't create invite link for object without group");
}
const team = new Team(
cojsonInternals.expectTeamContent(currentCoValue.getCurrentContent()),
const group = new Group(
cojsonInternals.expectGroupContent(currentCoValue.getCurrentContent()),
node
);
const inviteSecret = team.createInvite(role);
const inviteSecret = group.createInvite(role);
return `${baseURL}#invitedTo=${value.id}&inviteSecret=${inviteSecret}`;
return `${baseURL}#invitedTo=${value.id}&${inviteSecret}`;
}
export function parseInviteLink(inviteURL: string):
@@ -278,8 +278,7 @@ export function parseInviteLink(inviteURL: string):
.split("&")[0]
?.replace(/^#invitedTo=/, "") as CoID<ContentType>;
const inviteSecret = url.hash
.split("&")[1]
?.replace(/^inviteSecret=/, "") as InviteSecret;
.split("&")[1] as InviteSecret;
if (!valueID || !inviteSecret) {
return undefined;
}

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react-auth-local",
"version": "0.0.13",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"jazz-browser-auth-local": "^0.0.6",
"jazz-react": "^0.0.16",
"jazz-browser-auth-local": "^0.1.0",
"jazz-react": "^0.1.0",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -1,12 +1,12 @@
{
"name": "jazz-react",
"version": "0.0.16",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.0.23",
"jazz-browser": "^0.0.6",
"cojson": "^0.1.0",
"jazz-browser": "^0.1.0",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -106,7 +106,6 @@ export function useTelepathicState<T extends ContentType>(id?: CoID<T>) {
// "Got update",
// id,
// newState.toJSON(),
// newState.coValue.sessions
// );
setState(newState as T);
});

View File

@@ -1,11 +1,11 @@
{
"name": "jazz-storage-indexeddb",
"version": "0.0.10",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.0.23",
"cojson": "^0.1.0",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -16,7 +16,7 @@ test.skip("Should be able to initialize and load from empty DB", async () => {
console.log("yay!");
const _team = node.createTeam();
const _group = node.createGroup();
await new Promise((resolve) => setTimeout(resolve, 200));
@@ -39,9 +39,9 @@ test("Should be able to sync data to database and then load that from a new node
console.log("yay!");
const team = node1.createTeam();
const group = node1.createGroup();
const map = team.createMap();
const map = group.createMap();
map.edit((m) => {
m.set("hello", "world");

View File

@@ -205,7 +205,7 @@ export class IDBStorage {
}
const dependedOnCoValues =
coValueRow?.header.ruleset.type === "team"
coValueRow?.header.ruleset.type === "group"
? Object.values(newContent.new).flatMap((sessionEntry) =>
sessionEntry.newTransactions.flatMap((tx) => {
if (tx.privacy !== "trusting") return [];
@@ -226,8 +226,8 @@ export class IDBStorage {
);
})
)
: coValueRow?.header.ruleset.type === "ownedByTeam"
? [coValueRow?.header.ruleset.team]
: coValueRow?.header.ruleset.type === "ownedByGroup"
? [coValueRow?.header.ruleset.group]
: [];
for (const dependedOnCoValue of dependedOnCoValues) {

View File

@@ -7744,6 +7744,11 @@ unbzip2-stream@1.4.3:
buffer "^5.2.1"
through "^2.3.8"
uniqolor@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/uniqolor/-/uniqolor-1.1.0.tgz#7519f81133cd54a1f4a59c33c81dbe04a3ad155d"
integrity sha512-j2XyokF24fsj+L5u6fbu4rM3RQc6VWJuAngYM2k0ZdG3yiVxt0smLkps2GmQIYqK8VkELGdM9vFU/HfOkK/zoQ==
unique-filename@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea"