Compare commits

...

16 Commits

Author SHA1 Message Date
Anselm
4a617c8323 Publish
- jazz-example-todo@0.0.22
 - cojson@0.1.7
 - cojson-simple-sync@0.1.8
 - cojson-storage-sqlite@0.1.5
 - jazz-browser@0.1.7
 - jazz-browser-auth-local@0.1.7
 - jazz-react@0.1.8
 - jazz-react-auth-local@0.1.8
 - jazz-storage-indexeddb@0.1.7
2023-09-05 17:25:24 +01:00
Anselm
eaed275a79 Stop tracing IDBStorage 2023-09-05 17:24:12 +01:00
Anselm
01fdcaed34 Hide AgentID and other internals from public API 2023-09-05 17:22:41 +01:00
Anselm
7aeb1a789b Replace getLastEditor with whoEdited/whoInserted 2023-09-05 16:54:57 +01:00
Anselm
a00649fa29 Add map, filter, reduce to CoList 2023-09-05 16:45:51 +01:00
Anselm
764954c727 Introduce ContentType.group 2023-09-05 16:38:07 +01:00
Anselm
b0ec93eb3a Make consumeInviteLinkFromWindowLocation generic 2023-09-05 16:33:15 +01:00
Anselm
4dd226bc95 Make more stuff in LocalNode private 2023-09-05 15:56:24 +01:00
Anselm
1692340856 Document CoList 2023-09-05 13:50:58 +01:00
Anselm
fbda78f908 Add/update docs for createMap/List 2023-09-05 13:38:43 +01:00
Anselm Eickhoff
61e9f6afad Merge pull request #49 from gardencmp/anselm-gar-67
Implement CoList
2023-09-05 13:28:44 +01:00
Anselm
246bbb119d Update walkthrough 2023-09-05 13:25:13 +01:00
Anselm
80054515c9 Enable strict mode again 2023-09-05 13:10:03 +01:00
Anselm
f9486a82c3 Publish
- jazz-example-todo@0.0.21
 - cojson@0.1.6
 - cojson-simple-sync@0.1.7
 - cojson-storage-sqlite@0.1.4
 - jazz-browser@0.1.6
 - jazz-browser-auth-local@0.1.6
 - jazz-react@0.1.7
 - jazz-react-auth-local@0.1.7
 - jazz-storage-indexeddb@0.1.6
2023-09-05 13:05:03 +01:00
Anselm
d0babab822 Remove log 2023-09-05 13:04:50 +01:00
Anselm Eickhoff
fe1092ccf6 Merge pull request #46 from gardencmp/anselm-gar-71 2023-09-04 21:58:19 +01:00
31 changed files with 403 additions and 196 deletions

172
README.md
View File

@@ -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)

View File

@@ -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" />
),
})
);
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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>
);

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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 () => {

View File

@@ -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_");
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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;

View File

@@ -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

View File

@@ -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}`;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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");
});

View File

@@ -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"))

View File

@@ -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,
},
});

View File

@@ -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) {

View File

@@ -10,6 +10,7 @@
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"stripInternal": true
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.test.*"],

View File

@@ -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": {

View File

@@ -147,7 +147,7 @@ async function signUp(
accountSecret,
} satisfies SessionStorageData);
node.ownSessionID = await getSessionFor(accountID);
node.currentSessionID = await getSessionFor(accountID);
return node;
}

View File

@@ -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": {

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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>>>();

View File

@@ -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": {