Compare commits
16 Commits
cojson@0.1
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a617c8323 | ||
|
|
eaed275a79 | ||
|
|
01fdcaed34 | ||
|
|
7aeb1a789b | ||
|
|
a00649fa29 | ||
|
|
764954c727 | ||
|
|
b0ec93eb3a | ||
|
|
4dd226bc95 | ||
|
|
1692340856 | ||
|
|
fbda78f908 | ||
|
|
61e9f6afad | ||
|
|
246bbb119d | ||
|
|
80054515c9 | ||
|
|
f9486a82c3 | ||
|
|
d0babab822 | ||
|
|
fe1092ccf6 |
172
README.md
172
README.md
@@ -164,7 +164,7 @@ Returns the role of the current account in the group.
|
||||
|
||||
```typescript
|
||||
addMember(
|
||||
accountID: AccountIDOrAgentID,
|
||||
accountID: AccountID,
|
||||
role: "reader" | "writer" | "admin"
|
||||
)
|
||||
```
|
||||
@@ -189,15 +189,22 @@ Strips the specified member of all roles (preventing future writes) and rotates
|
||||
|
||||
#### `Group.createMap(meta?)`
|
||||
```typescript
|
||||
createMap<
|
||||
M extends { [key: string]: JsonValue },
|
||||
Meta extends JsonObject | null = null
|
||||
>(meta?: Meta): CoMap<M, Meta>
|
||||
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
||||
meta?: M["meta"]
|
||||
): M
|
||||
```
|
||||
|
||||
Creates a new `CoMap` within this group, with the specified inner content type `M` and optional static metadata.
|
||||
Creates a new `CoMap` within this group, with the specified specialized `CoMap` type `M` and optional static metadata.
|
||||
|
||||
#### `Group.createList(meta?)`
|
||||
```typescript
|
||||
createList<L extends CoList<JsonValue, JsonObject | null>>(
|
||||
meta?: L["meta"]
|
||||
): L
|
||||
```
|
||||
|
||||
Creates a new `CoList` within this group, with the specified specialized `CoList` type `L` and optional static metadata.
|
||||
|
||||
#### `Group.createList(meta?)` (coming soon)
|
||||
#### `Group.createStream(meta?)` (coming soon)
|
||||
#### `Group.createStatic(meta)` (coming soon)
|
||||
|
||||
@@ -219,6 +226,14 @@ id: CoID<CoMap<M, Meta>>
|
||||
|
||||
Returns the CoMap's (precisely typed) `CoID`
|
||||
|
||||
#### `CoMap.meta`
|
||||
|
||||
```typescript
|
||||
meta: Meta
|
||||
```
|
||||
|
||||
Returns the CoMap's (precisely typed) static metadata
|
||||
|
||||
#### `CoMap.keys()`
|
||||
|
||||
```typescript
|
||||
@@ -233,10 +248,10 @@ get<K extends keyof M>(key: K): M[K] | undefined
|
||||
|
||||
Returns the current value for the given key.
|
||||
|
||||
#### `CoMap.getLastEditor(key)`
|
||||
#### `CoMap.whoEdited(key)`
|
||||
|
||||
```typescript
|
||||
getLastEditor<K extends keyof M>(key: K): AccountID | undefined
|
||||
whoEdited<K extends keyof M>(key: K): AccountID | undefined
|
||||
```
|
||||
|
||||
Returns the accountID of the last account to modify the value for the given key.
|
||||
@@ -307,10 +322,145 @@ If `privacy` is `"private"` **(default)**, `key` is encrypted in the transaction
|
||||
If `privacy` is `"trusting"`, `key` is stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
|
||||
|
||||
---
|
||||
### `CoValue` ContentType: `CoList` (not yet implemented)
|
||||
### `CoValue` ContentType: `CoList`
|
||||
|
||||
```typescript
|
||||
class CoList<
|
||||
T extends JsonValue,
|
||||
Meta extends JsonObject | null = null
|
||||
>
|
||||
```
|
||||
|
||||
#### `CoList.id`
|
||||
|
||||
```typescript
|
||||
id: CoID<CoList<T, Meta>>
|
||||
```
|
||||
|
||||
Returns the CoList's (precisely typed) `CoID`
|
||||
|
||||
#### `CoList.meta`
|
||||
|
||||
```typescript
|
||||
meta: Meta
|
||||
```
|
||||
|
||||
Returns the CoList's (precisely typed) static metadata
|
||||
|
||||
### `CoList.asArray()`
|
||||
|
||||
```typescript
|
||||
asArray(): T[]
|
||||
```
|
||||
|
||||
Returns the current items in the CoList as an array.
|
||||
|
||||
### `CoList.toJSON()`
|
||||
|
||||
```typescript
|
||||
toJSON(): T[]
|
||||
```
|
||||
|
||||
Returns the current items in the CoList as an array. (alias of asArray)
|
||||
|
||||
#### `CoList.whoInserted(idx)`
|
||||
|
||||
```typescript
|
||||
whoInserted(idx: number): AccountID | undefined
|
||||
```
|
||||
|
||||
Returns the accountID of the account that inserted value at the given index.
|
||||
|
||||
#### `CoList.subscribe(listener)`
|
||||
|
||||
```typescript
|
||||
subscribe(
|
||||
listener: (coMap: CoList<T, Meta>) => void
|
||||
): () => void
|
||||
```
|
||||
Lets you subscribe to future updates to this CoList (whether made locally or by other users). Takes a listener function that will be called with the current state for each update. Returns an unsubscribe function.
|
||||
|
||||
Used internally by `useTelepathicData()` for reactive updates on changes to a `CoList`.
|
||||
|
||||
#### `CoList.edit(editable => {...})`
|
||||
|
||||
```typescript
|
||||
edit(changer: (editable: WriteableCoList<T, Meta>) => void): CoList<T, Meta>
|
||||
```
|
||||
|
||||
Lets you apply edits to a `CoList`, inside the changer callback, which receives a `WriteableCoList`. A `WritableCoList` has all the same methods as a `CoList`, but all edits made to it with `append`, `push`, `prepend` or `delete` are reflected in it immediately - so it behaves mutably, whereas a `CoList` is always immutable (you need to use `subscribe` to receive new versions of it).
|
||||
|
||||
```typescript
|
||||
export class WriteableCoList<
|
||||
T extends JsonValue,
|
||||
Meta extends JsonObject | null = null,
|
||||
> extends CoList<T, Meta>
|
||||
```
|
||||
|
||||
#### `WritableCoList.append(after, value)`
|
||||
|
||||
```typescript
|
||||
append(
|
||||
after: number,
|
||||
value: T,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void
|
||||
```
|
||||
|
||||
Appends a new item after index `after`.
|
||||
|
||||
If `privacy` is `"private"` **(default)**, both `value` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
||||
|
||||
If `privacy` is `"trusting"`, both `value` is stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
|
||||
|
||||
#### `WritableCoList.prepend(after, value)`
|
||||
|
||||
```typescript
|
||||
prepend(
|
||||
before: number,
|
||||
value: T,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void
|
||||
```
|
||||
|
||||
Prepends a new item before index `before`.
|
||||
|
||||
If `privacy` is `"private"` **(default)**, both `value` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
||||
|
||||
If `privacy` is `"trusting"`, both `value` is stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
|
||||
|
||||
#### `WritableCoList.push(value)`
|
||||
|
||||
```typescript
|
||||
push(
|
||||
value: T,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void
|
||||
```
|
||||
|
||||
Pushes a new item to the end of the list.
|
||||
|
||||
If `privacy` is `"private"` **(default)**, both `value` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
||||
|
||||
If `privacy` is `"trusting"`, both `value` is stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
|
||||
|
||||
#### `WritableCoList.delete(at)`
|
||||
|
||||
```typescript
|
||||
delete(
|
||||
at: number,
|
||||
privacy: "private" | "trusting" = "private"
|
||||
): void
|
||||
```
|
||||
|
||||
Deletes the item at index `at` from the list.
|
||||
|
||||
If `privacy` is `"private"` **(default)**, the fact of this deletion is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
||||
|
||||
If `privacy` is `"trusting"`, the fact of this deletion is stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
|
||||
|
||||
---
|
||||
### `CoValue` ContentType: `CoStram` (not yet implemented)
|
||||
### `CoValue` ContentType: `CoStream` (not yet implemented)
|
||||
|
||||
---
|
||||
### `CoValue` ContentType: `Static` (not yet implemented)
|
||||
|
||||
@@ -57,20 +57,19 @@ import {
|
||||
|
||||
// ...
|
||||
|
||||
type TaskContent = { done: boolean; text: string };
|
||||
type Task = CoMap<TaskContent>;
|
||||
type Task = CoMap<{ done: boolean; text: string }>;
|
||||
|
||||
type TodoListContent = {
|
||||
type ListOfTasks = CoList<CoID<Task>>;
|
||||
|
||||
type TodoList = CoMap<{
|
||||
title: string;
|
||||
// other keys form a set of task IDs
|
||||
[taskId: CoID<Task>]: true;
|
||||
};
|
||||
type TodoList = CoMap<TodoListContent>;
|
||||
tasks: CoID<ListOfTasks>;
|
||||
}>;
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
First, we define our main data model of tasks and todo lists, using CoJSON's collaborative map type, `CoMap`. We reference CoMaps of individual tasks by using them as keys inside the `TodoList` CoMap - as a makeshift solution until `CoList` is implemented.
|
||||
First, we define our main data model of tasks and todo lists, using CoJSON's collaborative map and list types, `CoMap` & `CoList`.
|
||||
|
||||
---
|
||||
|
||||
@@ -105,11 +104,14 @@ export default function App() {
|
||||
|
||||
const createList = useCallback(
|
||||
(title: string) => {
|
||||
if (!title) return;
|
||||
const listGroup = localNode.createGroup();
|
||||
const list = listGroup.createMap<TodoListContent>();
|
||||
const list = listGroup.createMap<TodoList>();
|
||||
const tasks = listGroup.createList<ListOfTasks>();
|
||||
|
||||
list.edit((list) => {
|
||||
list.set("title", title);
|
||||
list.set("tasks", tasks.id);
|
||||
});
|
||||
|
||||
window.location.hash = list.id;
|
||||
@@ -157,18 +159,19 @@ If we have no `listId` set, the user can use the displayed creation input to cre
|
||||
```typescript
|
||||
export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
const list = useTelepathicState(listId);
|
||||
const tasks = useTelepathicState(list?.get("tasks"));
|
||||
|
||||
const createTask = (text: string) => {
|
||||
if (!list) return;
|
||||
const task = list.coValue.getGroup().createMap<TaskContent>();
|
||||
if (!tasks || !text) return;
|
||||
const task = tasks.group.createMap<Task>();
|
||||
|
||||
task.edit((task) => {
|
||||
task.set("text", text);
|
||||
task.set("done", false);
|
||||
});
|
||||
|
||||
list.edit((list) => {
|
||||
list.set(task.id, true);
|
||||
tasks.edit((tasks) => {
|
||||
tasks.push(task.id);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -195,12 +198,9 @@ export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list &&
|
||||
list
|
||||
.keys()
|
||||
.filter((key): key is CoID<Task> =>
|
||||
key.startsWith("co_")
|
||||
)
|
||||
{tasks &&
|
||||
tasks
|
||||
.asArray()
|
||||
.map((taskId) => (
|
||||
<TaskRow key={taskId} taskId={taskId} />
|
||||
))}
|
||||
@@ -224,11 +224,11 @@ export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
}
|
||||
```
|
||||
|
||||
Here in `<TodoListComponent>`, we use `useTelepathicData()` for the first time, in this case to load the CoValue for our `TodoList` and to reactively subscribe to updates to its content - whether we create edits locally, load persisted data, or receive sync updates from other devices or participants!
|
||||
Here in `<TodoListComponent>`, we use `useTelepathicData()` for the first time, in this case to load the CoValue for our `TodoList` as well as the `ListOfTasks` referenced in it. `useTelepathicData()` 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!
|
||||
|
||||
`createTask` is similar to `createList` we saw earlier, creating a new CoMap for a new task, and then adding it as a key to our `TodoList`.
|
||||
`createTask` is similar to `createList` we saw earlier, creating a new CoMap for a new task, and then adding it as an item to our `TodoList`'s `ListOfTasks`.
|
||||
|
||||
As you can see, we iterate over the keys of `TodoList` and for those that look like `CoID`s (they always start with `co_`), we render a `<TaskRow>`.
|
||||
As you can see, we iterate over the items of our `ListOfTasks` and render a `<TaskRow>` for each.
|
||||
|
||||
Below all tasks, we render a simple input for adding a task.
|
||||
|
||||
@@ -254,9 +254,11 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
<TableCell>
|
||||
<div className="flex flex-row justify-between items-center gap-2">
|
||||
<span className={task?.get("done") ? "line-through" : ""}>
|
||||
{task?.get("text") || <Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />}
|
||||
{task?.get("text") || (
|
||||
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
|
||||
)}
|
||||
</span>
|
||||
<NameBadge accountID={task?.getLastEditor("text")} />
|
||||
<NameBadge accountID={task?.whoEdited("text")} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -266,7 +268,7 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
|
||||
`<TaskRow>` uses `useTelepathicState()` as well, to granularly load and subscribe to changes for that particular task (the only thing we let the user change is the "done" status).
|
||||
|
||||
We also use a `<NameBadge>` helper component to render the name of the author of the task, which we get by using the collaboration feature `getLastEditor(key)` on our `Task` CoMap, which returns the accountID of the last account that changed a given key in the CoMap.
|
||||
We also use a `<NameBadge>` helper component to render the name of the author of the task, which we get by using the collaboration feature `whoEdited(key)` on our `Task` CoMap, which returns the accountID of the last account that changed a given key in the CoMap.
|
||||
|
||||
---
|
||||
|
||||
@@ -282,15 +284,19 @@ function NameBadge({ accountID }: { accountID?: AccountID }) {
|
||||
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
|
||||
|
||||
return (
|
||||
profile?.get("name") && <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>
|
||||
profile?.get("name") ? (
|
||||
<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>
|
||||
) : (
|
||||
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -307,22 +313,28 @@ function InviteButton({ list }: { list: TodoList }) {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
list.coValue.getGroup().myRole() === "admin" && (
|
||||
list.group.myRole() === "admin" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
disabled={!list}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
let inviteLink = existingInviteLink;
|
||||
if (list && !inviteLink) {
|
||||
inviteLink = createInviteLink(list, "writer");
|
||||
setExistingInviteLink(inviteLink);
|
||||
}
|
||||
if (inviteLink) {
|
||||
const qr = await QRCode.toDataURL(inviteLink, {
|
||||
errorCorrectionLevel: "L",
|
||||
});
|
||||
navigator.clipboard.writeText(inviteLink).then(() =>
|
||||
toast({
|
||||
description: "Copied invite link to clipboard!",
|
||||
title: "Copied invite link to clipboard!",
|
||||
description: (
|
||||
<img src={qr} className="w-20 h-20" />
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.22",
|
||||
"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.1.6",
|
||||
"jazz-react-auth-local": "^0.1.6",
|
||||
"jazz-react": "^0.1.8",
|
||||
"jazz-react-auth-local": "^0.1.8",
|
||||
"lucide-react": "^0.265.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -43,10 +43,10 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
const listener = async () => {
|
||||
const acceptedInvitation =
|
||||
await consumeInviteLinkFromWindowLocation(localNode);
|
||||
await consumeInviteLinkFromWindowLocation<TodoList>(localNode);
|
||||
|
||||
if (acceptedInvitation) {
|
||||
setListId(acceptedInvitation.valueID as CoID<TodoList>);
|
||||
setListId(acceptedInvitation.valueID);
|
||||
window.location.hash = acceptedInvitation.valueID;
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
|
||||
const createTask = (text: string) => {
|
||||
if (!tasks || !text) return;
|
||||
const task = tasks.coValue.getGroup().createMap<Task>();
|
||||
const task = tasks.group.createMap<Task>();
|
||||
|
||||
task.edit((task) => {
|
||||
task.set("text", text);
|
||||
@@ -143,12 +143,9 @@ export function TodoListComponent({ listId }: { listId: CoID<TodoList> }) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks &&
|
||||
tasks
|
||||
.asArray()
|
||||
.map((taskId) => (
|
||||
<TaskRow key={taskId} taskId={taskId} />
|
||||
))}
|
||||
{tasks?.map((taskId) => (
|
||||
<TaskRow key={taskId} taskId={taskId} />
|
||||
))}
|
||||
<TableRow key="new">
|
||||
<TableCell>
|
||||
<Checkbox className="mt-1" disabled />
|
||||
@@ -191,7 +188,7 @@ function TaskRow({ taskId }: { taskId: CoID<Task> }) {
|
||||
<Skeleton className="mt-1 w-[200px] h-[1em] rounded-full" />
|
||||
)}
|
||||
</span>
|
||||
<NameBadge accountID={task?.getLastEditor("text")} />
|
||||
<NameBadge accountID={task?.whoEdited("text")} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -208,20 +205,18 @@ function NameBadge({ accountID }: { accountID?: AccountID }) {
|
||||
const brightColor = uniqolor(accountID || "", { lightness: 80 }).color;
|
||||
const darkColor = uniqolor(accountID || "", { lightness: 20 }).color;
|
||||
|
||||
return (
|
||||
profile?.get("name") ? (
|
||||
<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>
|
||||
) : (
|
||||
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
|
||||
)
|
||||
return profile?.get("name") ? (
|
||||
<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>
|
||||
) : (
|
||||
<Skeleton className="mt-1 w-[50px] h-[1em] rounded-full" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -230,7 +225,7 @@ function InviteButton({ list }: { list: TodoList }) {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
list.coValue.getGroup().myRole() === "admin" && (
|
||||
list.group.myRole() === "admin" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
|
||||
@@ -11,7 +11,7 @@ import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
// <React.StrictMode>
|
||||
<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
|
||||
@@ -31,5 +31,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<Toaster />
|
||||
</WithJazz>
|
||||
</ThemeProvider>
|
||||
// </React.StrictMode>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
@@ -16,8 +16,8 @@
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.5",
|
||||
"cojson-storage-sqlite": "^0.1.3",
|
||||
"cojson": "^0.1.7",
|
||||
"cojson-storage-sqlite": "^0.1.5",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "^0.1.5",
|
||||
"cojson": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
@@ -34,7 +34,7 @@ test("A node with an account can create groups and and objects within them", asy
|
||||
|
||||
expect(map.get("foo")).toEqual("bar");
|
||||
|
||||
expect(map.getLastEditor("foo")).toEqual(accountID);
|
||||
expect(map.whoEdited("foo")).toEqual(accountID);
|
||||
});
|
||||
|
||||
test("Can create account with one node, and then load it on another", async () => {
|
||||
|
||||
@@ -52,7 +52,7 @@ export class Account extends Group {
|
||||
}
|
||||
|
||||
export interface GeneralizedControlledAccount {
|
||||
id: AccountIDOrAgentID;
|
||||
id: AccountID | AgentID;
|
||||
agentSecret: AgentSecret;
|
||||
|
||||
currentAgentID: () => AgentID;
|
||||
@@ -138,11 +138,7 @@ export type AccountMeta = { type: "account" };
|
||||
export type AccountMap = CoMap<AccountContent, AccountMeta>;
|
||||
export type AccountID = CoID<AccountMap>;
|
||||
|
||||
export type AccountIDOrAgentID = AgentID | AccountID;
|
||||
export type AccountOrAgentID = AgentID | Account;
|
||||
export type AccountOrAgentSecret = AgentSecret | Account;
|
||||
|
||||
export function isAccountID(id: AccountIDOrAgentID): id is AccountID {
|
||||
export function isAccountID(id: AccountID | AgentID): id is AccountID {
|
||||
return id.startsWith("co_");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ test("Can create coValue with new agent credentials and add transaction to it",
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[transaction]
|
||||
);
|
||||
|
||||
expect(
|
||||
coValue.tryAddTransactions(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
sign(account.currentSignerSecret(), expectedNewHash)
|
||||
@@ -65,13 +65,13 @@ test("transactions with wrong signature are rejected", () => {
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[transaction]
|
||||
);
|
||||
|
||||
expect(
|
||||
coValue.tryAddTransactions(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
sign(getAgentSignerSecret(wrongAgent), expectedNewHash)
|
||||
@@ -101,7 +101,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[
|
||||
{
|
||||
privacy: "trusting",
|
||||
@@ -117,7 +117,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
|
||||
expect(
|
||||
coValue.tryAddTransactions(
|
||||
node.ownSessionID,
|
||||
node.currentSessionID,
|
||||
[transaction],
|
||||
expectedNewHash,
|
||||
sign(account.currentSignerSecret(), expectedNewHash)
|
||||
@@ -170,7 +170,7 @@ test("New transactions in a group correctly update owned values, including subsc
|
||||
|
||||
expect(map.coValue.getValidSortedTransactions().length).toBe(1);
|
||||
|
||||
const manuallyAdddedTxSuccess = group.groupMap.coValue.tryAddTransactions(node.ownSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
|
||||
const manuallyAdddedTxSuccess = group.groupMap.coValue.tryAddTransactions(node.currentSessionID, [resignationThatWeJustLearnedAbout], expectedNewHash, signature);
|
||||
|
||||
expect(manuallyAdddedTxSuccess).toBe(true);
|
||||
|
||||
|
||||
@@ -30,11 +30,10 @@ import {
|
||||
import { Group, expectGroupContent } from "./group.js";
|
||||
import { LocalNode } from "./node.js";
|
||||
import { CoValueKnownState, NewContentMessage } from "./sync.js";
|
||||
import { RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
import { CoList } from "./contentTypes/coList.js";
|
||||
import {
|
||||
AccountID,
|
||||
AccountIDOrAgentID,
|
||||
GeneralizedControlledAccount,
|
||||
} from "./account.js";
|
||||
|
||||
@@ -53,11 +52,11 @@ export function idforHeader(header: CoValueHeader): RawCoID {
|
||||
|
||||
export function accountOrAgentIDfromSessionID(
|
||||
sessionID: SessionID
|
||||
): AccountIDOrAgentID {
|
||||
return sessionID.split("_session")[0] as AccountIDOrAgentID;
|
||||
): AccountID | AgentID {
|
||||
return sessionID.split("_session")[0] as AccountID | AgentID;
|
||||
}
|
||||
|
||||
export function newRandomSessionID(accountID: AccountIDOrAgentID): SessionID {
|
||||
export function newRandomSessionID(accountID: AccountID | AgentID): SessionID {
|
||||
return `${accountID}_session_z${base58.encode(randomBytes(8))}`;
|
||||
}
|
||||
|
||||
@@ -131,11 +130,11 @@ export class CoValue {
|
||||
|
||||
testWithDifferentAccount(
|
||||
account: GeneralizedControlledAccount,
|
||||
ownSessionID: SessionID
|
||||
currentSessionID: SessionID
|
||||
): CoValue {
|
||||
const newNode = this.node.testWithDifferentAccount(
|
||||
account,
|
||||
ownSessionID
|
||||
currentSessionID
|
||||
);
|
||||
|
||||
return newNode.expectCoValueLoaded(this.id);
|
||||
@@ -159,7 +158,7 @@ export class CoValue {
|
||||
}
|
||||
|
||||
nextTransactionID(): TransactionID {
|
||||
const sessionID = this.node.ownSessionID;
|
||||
const sessionID = this.node.currentSessionID;
|
||||
return {
|
||||
sessionID,
|
||||
txIndex: this.sessions[sessionID]?.transactions.length || 0,
|
||||
@@ -294,7 +293,7 @@ export class CoValue {
|
||||
};
|
||||
}
|
||||
|
||||
const sessionID = this.node.ownSessionID;
|
||||
const sessionID = this.node.currentSessionID;
|
||||
|
||||
const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
|
||||
transaction,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoID } from "../contentType.js";
|
||||
import { CoValue, accountOrAgentIDfromSessionID } from "../coValue.js";
|
||||
import { SessionID, TransactionID } from "../ids.js";
|
||||
import { AccountID } from "../index.js";
|
||||
import { AccountID, Group } from "../index.js";
|
||||
import { isAccountID } from "../account.js";
|
||||
|
||||
type OpID = TransactionID & { changeIdx: number };
|
||||
@@ -79,6 +79,10 @@ export class CoList<
|
||||
return this.coValue.header.meta as Meta;
|
||||
}
|
||||
|
||||
get group(): Group {
|
||||
return this.coValue.getGroup();
|
||||
}
|
||||
|
||||
protected fillOpsFromCoValue() {
|
||||
this.insertions = {};
|
||||
this.deletionsByInsertion = {};
|
||||
@@ -230,7 +234,7 @@ export class CoList<
|
||||
}
|
||||
}
|
||||
|
||||
getLastEditor(idx: number): AccountID | undefined {
|
||||
whoInserted(idx: number): AccountID | undefined {
|
||||
const entry = this.entries()[idx];
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
@@ -251,6 +255,28 @@ export class CoList<
|
||||
return this.entries().map((entry) => entry.value);
|
||||
}
|
||||
|
||||
map<U>(mapper: (value: T, idx: number) => U): U[] {
|
||||
return this.entries().map((entry, idx) => mapper(entry.value, idx));
|
||||
}
|
||||
|
||||
filter<U extends T>(predicate: (value: T, idx: number) => value is U): U[]
|
||||
filter(predicate: (value: T, idx: number) => boolean): T[] {
|
||||
return this.entries()
|
||||
.filter((entry, idx) => predicate(entry.value, idx))
|
||||
.map((entry) => entry.value);
|
||||
}
|
||||
|
||||
reduce<U>(
|
||||
reducer: (accumulator: U, value: T, idx: number) => U,
|
||||
initialValue: U
|
||||
): U {
|
||||
return this.entries().reduce(
|
||||
(accumulator, entry, idx) =>
|
||||
reducer(accumulator, entry.value, idx),
|
||||
initialValue
|
||||
);
|
||||
}
|
||||
|
||||
edit(
|
||||
changer: (editable: WriteableCoList<T, Meta>) => void
|
||||
): CoList<T, Meta> {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TransactionID } from '../ids.js';
|
||||
import { CoID } from '../contentType.js';
|
||||
import { CoValue, accountOrAgentIDfromSessionID } from '../coValue.js';
|
||||
import { AccountID, isAccountID } from '../account.js';
|
||||
import { Group } from '../group.js';
|
||||
|
||||
type MapOp<K extends string, V extends JsonValue> = {
|
||||
txID: TransactionID;
|
||||
@@ -50,6 +51,10 @@ export class CoMap<
|
||||
return this.coValue.header.meta as Meta;
|
||||
}
|
||||
|
||||
get group(): Group {
|
||||
return this.coValue.getGroup();
|
||||
}
|
||||
|
||||
protected fillOpsFromCoValue() {
|
||||
this.ops = {};
|
||||
|
||||
@@ -111,7 +116,7 @@ export class CoMap<
|
||||
}
|
||||
}
|
||||
|
||||
getLastEditor<K extends MapK<M>>(key: K): AccountID | undefined {
|
||||
whoEdited<K extends MapK<M>>(key: K): AccountID | undefined {
|
||||
const tx = this.getLastTxID(key);
|
||||
if (!tx) {
|
||||
return undefined;
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
getAgentID,
|
||||
} from "./crypto.js";
|
||||
import { LocalNode } from "./node.js";
|
||||
import { SessionID, isAgentID } from "./ids.js";
|
||||
import { AgentID, SessionID, isAgentID } from "./ids.js";
|
||||
import {
|
||||
AccountIDOrAgentID,
|
||||
AccountID,
|
||||
GeneralizedControlledAccount,
|
||||
Profile,
|
||||
} from "./account.js";
|
||||
@@ -28,9 +28,9 @@ import { CoList } from "./contentTypes/coList.js";
|
||||
|
||||
export type GroupContent = {
|
||||
profile: CoID<Profile> | null;
|
||||
[key: AccountIDOrAgentID]: Role;
|
||||
[key: AccountID | AgentID]: Role;
|
||||
readKey: KeyID;
|
||||
[revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed<KeySecret>;
|
||||
[revelationFor: `${KeyID}_for_${AccountID | AgentID}`]: Sealed<KeySecret>;
|
||||
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
||||
KeySecret,
|
||||
{ encryptedID: KeyID; encryptingID: KeyID }
|
||||
@@ -63,15 +63,25 @@ export class Group {
|
||||
return this.groupMap.id;
|
||||
}
|
||||
|
||||
roleOf(accountID: AccountIDOrAgentID): Role | undefined {
|
||||
roleOf(accountID: AccountID): Role | undefined {
|
||||
return this.roleOfInternal(accountID);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
roleOfInternal(accountID: AccountID | AgentID): Role | undefined {
|
||||
return this.groupMap.get(accountID);
|
||||
}
|
||||
|
||||
myRole(): Role | undefined {
|
||||
return this.roleOf(this.node.account.id);
|
||||
return this.roleOfInternal(this.node.account.id);
|
||||
}
|
||||
|
||||
addMember(accountID: AccountIDOrAgentID, role: Role) {
|
||||
addMember(accountID: AccountID, role: Role) {
|
||||
this.addMemberInternal(accountID, role);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
addMemberInternal(accountID: AccountID | AgentID, role: Role) {
|
||||
this.groupMap = this.groupMap.edit((map) => {
|
||||
const currentReadKey = this.groupMap.coValue.getCurrentReadKey();
|
||||
|
||||
@@ -112,7 +122,7 @@ export class Group {
|
||||
const inviteSecret = agentSecretFromSecretSeed(secretSeed);
|
||||
const inviteID = getAgentID(inviteSecret);
|
||||
|
||||
this.addMember(inviteID, `${role}Invite` as Role);
|
||||
this.addMemberInternal(inviteID, `${role}Invite` as Role);
|
||||
|
||||
return inviteSecretFromSecretSeed(secretSeed);
|
||||
}
|
||||
@@ -127,7 +137,7 @@ export class Group {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) as AccountIDOrAgentID[];
|
||||
}) as (AccountID | AgentID)[];
|
||||
|
||||
const maybeCurrentReadKey = this.groupMap.coValue.getCurrentReadKey();
|
||||
|
||||
@@ -179,7 +189,12 @@ export class Group {
|
||||
});
|
||||
}
|
||||
|
||||
removeMember(accountID: AccountIDOrAgentID) {
|
||||
removeMember(accountID: AccountID) {
|
||||
this.removeMemberInternal(accountID);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
removeMemberInternal(accountID: AccountID | AgentID) {
|
||||
this.groupMap = this.groupMap.edit((map) => {
|
||||
map.set(accountID, "revoked", "trusting");
|
||||
});
|
||||
@@ -219,6 +234,7 @@ export class Group {
|
||||
.getCurrentContent() as L;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
testWithDifferentAccount(
|
||||
account: GeneralizedControlledAccount,
|
||||
sessionId: SessionID
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountIDOrAgentID } from './account.js';
|
||||
import { AccountID } from './account.js';
|
||||
import { base58 } from "@scure/base";
|
||||
import { shortHashLength } from './crypto.js';
|
||||
|
||||
@@ -23,4 +23,4 @@ export function isAgentID(id: string): id is AgentID {
|
||||
return typeof id === "string" && id.startsWith("sealer_") && id.includes("/signer_");
|
||||
}
|
||||
|
||||
export type SessionID = `${AccountIDOrAgentID}_session_z${string}`;
|
||||
export type SessionID = `${AccountID | AgentID}_session_z${string}`;
|
||||
|
||||
@@ -86,5 +86,4 @@ export namespace CojsonInternalTypes {
|
||||
export type Transaction = import("./coValue.js").Transaction;
|
||||
export type Signature = import("./crypto.js").Signature;
|
||||
export type RawCoID = import("./ids.js").RawCoID;
|
||||
export type AccountIDOrAgentID = import("./account.js").AccountIDOrAgentID;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CoID, ContentType } from "./contentType.js";
|
||||
import {
|
||||
Account,
|
||||
AccountMeta,
|
||||
AccountIDOrAgentID,
|
||||
accountHeaderForInitialAgentSecret,
|
||||
GeneralizedControlledAccount,
|
||||
ControlledAccount,
|
||||
@@ -36,17 +35,19 @@ import {
|
||||
import { CoMap } from "./index.js";
|
||||
|
||||
export class LocalNode {
|
||||
/** @internal */
|
||||
coValues: { [key: RawCoID]: CoValueState } = {};
|
||||
/** @internal */
|
||||
account: GeneralizedControlledAccount;
|
||||
ownSessionID: SessionID;
|
||||
currentSessionID: SessionID;
|
||||
sync = new SyncManager(this);
|
||||
|
||||
constructor(
|
||||
account: GeneralizedControlledAccount,
|
||||
ownSessionID: SessionID
|
||||
currentSessionID: SessionID
|
||||
) {
|
||||
this.account = account;
|
||||
this.ownSessionID = ownSessionID;
|
||||
this.currentSessionID = currentSessionID;
|
||||
}
|
||||
|
||||
static withNewlyCreatedAccount(
|
||||
@@ -75,7 +76,7 @@ export class LocalNode {
|
||||
node: nodeWithAccount,
|
||||
accountID: account.id,
|
||||
accountSecret: account.agentSecret,
|
||||
sessionID: nodeWithAccount.ownSessionID,
|
||||
sessionID: nodeWithAccount.currentSessionID,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ export class LocalNode {
|
||||
return node;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
createCoValue(header: CoValueHeader): CoValue {
|
||||
const coValue = new CoValue(header, this);
|
||||
this.coValues[coValue.id] = { state: "loaded", coValue: coValue };
|
||||
@@ -118,6 +120,7 @@ export class LocalNode {
|
||||
return coValue;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
loadCoValue(id: RawCoID): Promise<CoValue> {
|
||||
let entry = this.coValues[id];
|
||||
if (!entry) {
|
||||
@@ -210,7 +213,7 @@ export class LocalNode {
|
||||
newRandomSessionID(inviteAgentID)
|
||||
);
|
||||
|
||||
groupAsInvite.addMember(
|
||||
groupAsInvite.addMemberInternal(
|
||||
this.account.id,
|
||||
inviteRole === "adminInvite"
|
||||
? "admin"
|
||||
@@ -227,6 +230,7 @@ export class LocalNode {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
expectCoValueLoaded(id: RawCoID, expectation?: string): CoValue {
|
||||
const entry = this.coValues[id];
|
||||
if (!entry) {
|
||||
@@ -244,6 +248,7 @@ export class LocalNode {
|
||||
return entry.coValue;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
expectProfileLoaded(id: AccountID, expectation?: string): Profile {
|
||||
const account = this.expectCoValueLoaded(id, expectation);
|
||||
const profileID = expectGroupContent(account.getCurrentContent()).get(
|
||||
@@ -262,6 +267,7 @@ export class LocalNode {
|
||||
).getCurrentContent() as Profile;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
createAccount(
|
||||
name: string,
|
||||
agentSecret = newRandomAgentSecret()
|
||||
@@ -326,7 +332,8 @@ export class LocalNode {
|
||||
return controlledAccount;
|
||||
}
|
||||
|
||||
resolveAccountAgent(id: AccountIDOrAgentID, expectation?: string): AgentID {
|
||||
/** @internal */
|
||||
resolveAccountAgent(id: AccountID | AgentID, expectation?: string): AgentID {
|
||||
if (isAgentID(id)) {
|
||||
return id;
|
||||
}
|
||||
@@ -388,11 +395,12 @@ export class LocalNode {
|
||||
return new Group(groupContent, this);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
testWithDifferentAccount(
|
||||
account: GeneralizedControlledAccount,
|
||||
ownSessionID: SessionID
|
||||
currentSessionID: SessionID
|
||||
): LocalNode {
|
||||
const newNode = new LocalNode(account, ownSessionID);
|
||||
const newNode = new LocalNode(account, currentSessionID);
|
||||
|
||||
const coValuesToCopy = Object.entries(this.coValues);
|
||||
|
||||
@@ -429,6 +437,7 @@ export class LocalNode {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
type CoValueState =
|
||||
| {
|
||||
state: "loading";
|
||||
@@ -437,6 +446,7 @@ type CoValueState =
|
||||
}
|
||||
| { state: "loaded"; coValue: CoValue };
|
||||
|
||||
/** @internal */
|
||||
export function newLoadingState(): CoValueState {
|
||||
let resolve: (coValue: CoValue) => void;
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ test("Admins can't demote other admins in a group (high level)", () => {
|
||||
newRandomSessionID(otherAdmin.id)
|
||||
);
|
||||
|
||||
expect(() => groupAsOtherAdmin.addMember(admin.id, "writer")).toThrow(
|
||||
expect(() => groupAsOtherAdmin.addMemberInternal(admin.id, "writer")).toThrow(
|
||||
"Failed to set role"
|
||||
);
|
||||
|
||||
@@ -1378,7 +1378,7 @@ test("Admins can create an adminInvite, which can add an admin (high-level)", as
|
||||
groupAsInvitedAdmin.groupMap.coValue.getCurrentReadKey().secret
|
||||
).toBeDefined();
|
||||
|
||||
groupAsInvitedAdmin.addMember(thirdAdminID, "admin");
|
||||
groupAsInvitedAdmin.addMemberInternal(thirdAdminID, "admin");
|
||||
|
||||
expect(groupAsInvitedAdmin.groupMap.get(thirdAdminID)).toEqual("admin");
|
||||
});
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
TrustingTransaction,
|
||||
accountOrAgentIDfromSessionID,
|
||||
} from "./coValue.js";
|
||||
import { RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
import {
|
||||
AccountIDOrAgentID,
|
||||
AccountID,
|
||||
Profile,
|
||||
} from "./account.js";
|
||||
|
||||
export type PermissionsDef =
|
||||
| { type: "group"; initialAdmin: AccountIDOrAgentID }
|
||||
| { type: "group"; initialAdmin: AccountID | AgentID }
|
||||
| { type: "ownedByGroup"; group: RawCoID }
|
||||
| { type: "unsafeAllowAll" };
|
||||
|
||||
@@ -63,7 +63,7 @@ export function determineValidTransactions(
|
||||
throw new Error("Group must have initialAdmin");
|
||||
}
|
||||
|
||||
const memberState: { [agent: AccountIDOrAgentID]: Role } = {};
|
||||
const memberState: { [agent: AccountID | AgentID]: Role } = {};
|
||||
|
||||
const validTransactions: { txID: TransactionID; tx: Transaction }[] =
|
||||
[];
|
||||
@@ -77,7 +77,7 @@ export function determineValidTransactions(
|
||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||
|
||||
const change = tx.changes[0] as
|
||||
| MapOpPayload<AccountIDOrAgentID, Role>
|
||||
| MapOpPayload<AccountID | AgentID, Role>
|
||||
| MapOpPayload<"readKey", JsonValue>
|
||||
| MapOpPayload<"profile", CoID<Profile>>;
|
||||
if (tx.changes.length !== 1) {
|
||||
@@ -248,7 +248,7 @@ export function isKeyForKeyField(
|
||||
|
||||
export function isKeyForAccountField(
|
||||
field: string
|
||||
): field is `${KeyID}_for_${AccountIDOrAgentID}` {
|
||||
): field is `${KeyID}_for_${AccountID | AgentID}` {
|
||||
return (
|
||||
field.startsWith("key_") &&
|
||||
(field.includes("_for_sealer") || field.includes("_for_co"))
|
||||
|
||||
@@ -77,12 +77,12 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
uniqueness: map.coValue.header.uniqueness,
|
||||
},
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 0,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -94,7 +94,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -130,7 +130,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
id: map.coValue.id,
|
||||
header: true,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 1,
|
||||
[node.currentSessionID]: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -155,12 +155,12 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
id: map.coValue.id,
|
||||
header: undefined,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 1,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -172,7 +172,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -207,7 +207,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
id: map.coValue.id,
|
||||
header: false,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 0,
|
||||
[node.currentSessionID]: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -244,12 +244,12 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
action: "content",
|
||||
id: map.coValue.id,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 0,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -261,7 +261,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -276,12 +276,12 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
action: "content",
|
||||
id: map.coValue.id,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 1,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -293,7 +293,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -332,7 +332,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
id: map.coValue.id,
|
||||
header: false,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 0,
|
||||
[node.currentSessionID]: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -355,12 +355,12 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
id: map.coValue.id,
|
||||
header: map.coValue.header,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 0,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -372,7 +372,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -403,7 +403,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
id: map.coValue.id,
|
||||
header: false,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 0,
|
||||
[node.currentSessionID]: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -447,7 +447,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
id: map.coValue.id,
|
||||
header: true,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 1,
|
||||
[node.currentSessionID]: 1,
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
|
||||
@@ -458,12 +458,12 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
id: map.coValue.id,
|
||||
header: undefined,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 1,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -475,7 +475,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -561,12 +561,12 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
id: map.coValue.id,
|
||||
header: map.coValue.header,
|
||||
new: {
|
||||
[node.ownSessionID]: {
|
||||
[node.currentSessionID]: {
|
||||
after: 0,
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.coValue.sessions[node.ownSessionID]!
|
||||
madeAt: map.coValue.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
{
|
||||
@@ -578,7 +578,7 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
map.coValue.sessions[node.ownSessionID]!.lastSignature!,
|
||||
map.coValue.sessions[node.currentSessionID]!.lastSignature!,
|
||||
},
|
||||
},
|
||||
} satisfies SyncMessage);
|
||||
@@ -697,7 +697,7 @@ test("When receiving a subscribe with a known state that is ahead of our own, pe
|
||||
id: map.coValue.id,
|
||||
header: true,
|
||||
sessions: {
|
||||
[node.ownSessionID]: 1,
|
||||
[node.currentSessionID]: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -258,7 +258,6 @@ export class SyncManager {
|
||||
const readIncoming = async () => {
|
||||
try {
|
||||
for await (const msg of peerState.incoming) {
|
||||
console.log("Got msg from", peerState.id, msg);
|
||||
try {
|
||||
await this.handleSyncMessage(msg, peerState);
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.test.*"],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-local",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.1.5",
|
||||
"jazz-browser": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -147,7 +147,7 @@ async function signUp(
|
||||
accountSecret,
|
||||
} satisfies SessionStorageData);
|
||||
|
||||
node.ownSessionID = await getSessionFor(accountID);
|
||||
node.currentSessionID = await getSessionFor(accountID);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.5",
|
||||
"jazz-storage-indexeddb": "^0.1.5",
|
||||
"cojson": "^0.1.7",
|
||||
"jazz-storage-indexeddb": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { InviteSecret } from "cojson";
|
||||
import {
|
||||
LocalNode,
|
||||
cojsonInternals,
|
||||
CojsonInternalTypes,
|
||||
AccountID,
|
||||
AgentID,
|
||||
SessionID,
|
||||
SyncMessage,
|
||||
Peer,
|
||||
@@ -39,7 +40,7 @@ export async function createBrowserNode({
|
||||
sessionDone = sessionHandle.done;
|
||||
return sessionHandle.session;
|
||||
},
|
||||
[await IDBStorage.asPeer({ trace: true }), firstWsPeer]
|
||||
[await IDBStorage.asPeer(), firstWsPeer]
|
||||
);
|
||||
|
||||
async function websocketReconnectLoop() {
|
||||
@@ -81,7 +82,7 @@ export interface AuthProvider {
|
||||
}
|
||||
|
||||
export type SessionProvider = (
|
||||
accountID: CojsonInternalTypes.AccountIDOrAgentID
|
||||
accountID: AccountID | AgentID
|
||||
) => Promise<SessionID>;
|
||||
|
||||
export type SessionHandle = {
|
||||
@@ -90,7 +91,7 @@ export type SessionHandle = {
|
||||
};
|
||||
|
||||
function getSessionHandleFor(
|
||||
accountID: CojsonInternalTypes.AccountIDOrAgentID
|
||||
accountID: AccountID | AgentID
|
||||
): SessionHandle {
|
||||
let done!: () => void;
|
||||
const donePromise = new Promise<void>((resolve) => {
|
||||
@@ -286,9 +287,7 @@ export function createInviteLink(
|
||||
let currentCoValue = coValue;
|
||||
|
||||
while (currentCoValue.header.ruleset.type === "ownedByGroup") {
|
||||
currentCoValue = node.expectCoValueLoaded(
|
||||
currentCoValue.header.ruleset.group
|
||||
);
|
||||
currentCoValue = currentCoValue.getGroup().groupMap.coValue;
|
||||
}
|
||||
|
||||
if (currentCoValue.header.ruleset.type !== "group") {
|
||||
@@ -305,16 +304,16 @@ export function createInviteLink(
|
||||
return `${baseURL}#invitedTo=${value.id}&${inviteSecret}`;
|
||||
}
|
||||
|
||||
export function parseInviteLink(inviteURL: string):
|
||||
export function parseInviteLink<C extends ContentType>(inviteURL: string):
|
||||
| {
|
||||
valueID: CoID<ContentType>;
|
||||
valueID: CoID<C>;
|
||||
inviteSecret: InviteSecret;
|
||||
}
|
||||
| undefined {
|
||||
const url = new URL(inviteURL);
|
||||
const valueID = url.hash
|
||||
.split("&")[0]
|
||||
?.replace(/^#invitedTo=/, "") as CoID<ContentType>;
|
||||
?.replace(/^#invitedTo=/, "") as CoID<C>;
|
||||
const inviteSecret = url.hash.split("&")[1] as InviteSecret;
|
||||
if (!valueID || !inviteSecret) {
|
||||
return undefined;
|
||||
@@ -322,15 +321,15 @@ export function parseInviteLink(inviteURL: string):
|
||||
return { valueID, inviteSecret };
|
||||
}
|
||||
|
||||
export function consumeInviteLinkFromWindowLocation(node: LocalNode): Promise<
|
||||
export function consumeInviteLinkFromWindowLocation<C extends ContentType>(node: LocalNode): Promise<
|
||||
| {
|
||||
valueID: string;
|
||||
valueID: CoID<C>;
|
||||
inviteSecret: string;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const result = parseInviteLink(window.location.href);
|
||||
const result = parseInviteLink<C>(window.location.href);
|
||||
|
||||
if (result) {
|
||||
node.acceptInvite(result.valueID, result.inviteSecret)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react-auth-local",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.1.5",
|
||||
"jazz-react": "^0.1.6",
|
||||
"jazz-browser-auth-local": "^0.1.7",
|
||||
"jazz-react": "^0.1.8",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.5",
|
||||
"jazz-browser": "^0.1.5",
|
||||
"cojson": "^0.1.7",
|
||||
"jazz-browser": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -125,7 +125,7 @@ export function useTelepathicState<T extends ContentType>(id?: CoID<T>) {
|
||||
}
|
||||
|
||||
export function useProfile<
|
||||
P extends { [key: string]: JsonValue } & ProfileContent = ProfileContent
|
||||
P extends ({ [key: string]: JsonValue } & ProfileContent) = ProfileContent
|
||||
>(accountID?: AccountID): CoMap<P, ProfileMeta> | undefined {
|
||||
const [profileID, setProfileID] = useState<CoID<CoMap<P, ProfileMeta>>>();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-storage-indexeddb",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.5",
|
||||
"cojson": "^0.1.7",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user