Compare commits
8 Commits
cojson-sim
...
cojson-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e8b0b9da | ||
|
|
c46a1f6b0a | ||
|
|
7947918278 | ||
|
|
50c36e7255 | ||
|
|
c39a7ed1b7 | ||
|
|
83762dbb0f | ||
|
|
7c82e12508 | ||
|
|
6db149be36 |
196
DOCS.md
196
DOCS.md
@@ -2,6 +2,17 @@
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
## `Media` (namespace in `cojson`)
|
||||
|
||||
```typescript
|
||||
export Media
|
||||
```
|
||||
TODO: document
|
||||
|
||||
TODO: doc generator not implemented yet
|
||||
|
||||
----
|
||||
|
||||
## `LocalNode` (class in `cojson`)
|
||||
@@ -340,7 +351,7 @@ Creates an invite for new members to indirectly join the group, allowing them to
|
||||
<summary><code>group.createMap(meta)</code> </summary>
|
||||
|
||||
```typescript
|
||||
group.createMap<M extends CoMap<{ [key: string]: JsonValue }, null | JsonObject>>(
|
||||
group.createMap<M extends CoMap<{ [key: string]: JsonValue | undefined }, null | JsonObject>>(
|
||||
meta: M["meta"]
|
||||
): M
|
||||
```
|
||||
@@ -405,7 +416,7 @@ TODO: document
|
||||
## `CoMap` (class in `cojson`)
|
||||
|
||||
```typescript
|
||||
export class CoMap<M extends { [key: string]: JsonValue }, Meta extends JsonObject | null> implements ReadableCoValue {...}
|
||||
export class CoMap<M extends { [key: string]: JsonValue | undefined }, Meta extends JsonObject | null> implements ReadableCoValue {...}
|
||||
```
|
||||
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||
|
||||
@@ -421,7 +432,7 @@ A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||
<summary><code>coMap.id</code> </summary>
|
||||
|
||||
```typescript
|
||||
coMap.id: CoID<CoMap<MapM<M>, Meta>>
|
||||
coMap.id: CoID<CoMap<M, Meta>>
|
||||
```
|
||||
The `CoValue`'s (precisely typed) `CoID`
|
||||
|
||||
@@ -655,7 +666,7 @@ Lets you apply edits to a `CoValue`, inside the changer callback, which receives
|
||||
## `WriteableCoMap` (class in `cojson`)
|
||||
|
||||
```typescript
|
||||
export class WriteableCoMap<M extends { [key: string]: JsonValue }, Meta extends JsonObject | null> extends CoMap<M, Meta> implements WriteableCoValue {...}
|
||||
export class WriteableCoMap<M extends { [key: string]: JsonValue | undefined }, Meta extends JsonObject | null> extends CoMap<M, Meta> implements WriteableCoValue {...}
|
||||
```
|
||||
A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||
|
||||
@@ -671,7 +682,7 @@ A collaborative map with precise shape `M` and optional static metadata `Meta`
|
||||
<summary><code>writeableCoMap.id</code> (from <code>CoMap</code>) </summary>
|
||||
|
||||
```typescript
|
||||
writeableCoMap.id: CoID<CoMap<MapM<M>, Meta>>
|
||||
writeableCoMap.id: CoID<CoMap<M, Meta>>
|
||||
```
|
||||
The `CoValue`'s (precisely typed) `CoID`
|
||||
|
||||
@@ -1574,7 +1585,7 @@ TODO: document
|
||||
<summary><code>coStream.items</code> (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
coStream.items: { [key: SessionID]: T[] }
|
||||
coStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -1628,6 +1639,44 @@ TODO: document
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>coStream.getLastItemsPerAccount()</code> (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
coStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>coStream.getLastItemFrom(account)</code> (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
coStream.getLastItemFrom(
|
||||
account: AccountID
|
||||
): undefined | T
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>coStream.getLastItemFromMe()</code> (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
coStream.getLastItemFromMe(): undefined | T
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>coStream.toJSON()</code> </summary>
|
||||
|
||||
@@ -1755,7 +1804,7 @@ TODO: document
|
||||
<summary><code>writeableCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableCoStream.items: { [key: SessionID]: T[] }
|
||||
writeableCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -1826,6 +1875,44 @@ TODO: document
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableCoStream.getLastItemsPerAccount()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableCoStream.getLastItemFrom(account)</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableCoStream.getLastItemFrom(
|
||||
account: AccountID
|
||||
): undefined | T
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableCoStream.getLastItemFromMe()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableCoStream.getLastItemFromMe(): undefined | T
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
||||
|
||||
@@ -1933,7 +2020,7 @@ TODO: document
|
||||
<summary><code>binaryCoStream.items</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
binaryCoStream.items: { [key: SessionID]: T[] }
|
||||
binaryCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -2019,6 +2106,44 @@ TODO: document
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>binaryCoStream.getLastItemsPerAccount()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
binaryCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>binaryCoStream.getLastItemFrom(account)</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
binaryCoStream.getLastItemFrom(
|
||||
account: AccountID
|
||||
): undefined | BinaryStreamItem
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>binaryCoStream.getLastItemFromMe()</code> (from <code>CoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
binaryCoStream.getLastItemFromMe(): undefined | BinaryStreamItem
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>binaryCoStream.toJSON()</code> (from <code>CoStream</code>) </summary>
|
||||
|
||||
@@ -2126,7 +2251,7 @@ TODO: document
|
||||
<summary><code>writeableBinaryCoStream.items</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableBinaryCoStream.items: { [key: SessionID]: T[] }
|
||||
writeableBinaryCoStream.items: { [key: SessionID]: {item: T, madeAt: number}[] }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -2240,6 +2365,44 @@ TODO: document
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableBinaryCoStream.getLastItemsPerAccount()</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableBinaryCoStream.getLastItemsPerAccount(): { [account: AccountID]: T | undefined }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableBinaryCoStream.getLastItemFrom(account)</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableBinaryCoStream.getLastItemFrom(
|
||||
account: AccountID
|
||||
): undefined | BinaryStreamItem
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableBinaryCoStream.getLastItemFromMe()</code> (from <code>BinaryCoStream</code>) (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
writeableBinaryCoStream.getLastItemFromMe(): undefined | BinaryStreamItem
|
||||
```
|
||||
TODO: document
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><code>writeableBinaryCoStream.toJSON()</code> (from <code>BinaryCoStream</code>) </summary>
|
||||
|
||||
@@ -2369,7 +2532,7 @@ TODO: document
|
||||
<summary><code>coValueCore._decryptionCache</code> (undocumented)</summary>
|
||||
|
||||
```typescript
|
||||
coValueCore._decryptionCache: { [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined }
|
||||
coValueCore._decryptionCache: { [key: Encrypted<JsonValue[], JsonValue>]: Stringified<JsonValue[]> | undefined }
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -2961,7 +3124,7 @@ TODO: doc generator not implemented yet
|
||||
## `CoValueImpl` (type alias in `cojson`)
|
||||
|
||||
```typescript
|
||||
export type CoValueImpl = CoMap<{ [key: string]: JsonValue }, JsonObject | null> | CoList<JsonValue, JsonObject | null> | CoStream<JsonValue, JsonObject | null> | BinaryCoStream<BinaryCoStreamMeta> | Static<JsonObject>
|
||||
export type CoValueImpl = CoMap<{ [key: string]: JsonValue | undefined }, JsonObject | null> | CoList<JsonValue, JsonObject | null> | CoStream<JsonValue, JsonObject | null> | BinaryCoStream<BinaryCoStreamMeta> | Static<JsonObject>
|
||||
```
|
||||
TODO: document
|
||||
|
||||
@@ -3157,17 +3320,6 @@ TODO: doc generator not implemented yet
|
||||
|
||||
----
|
||||
|
||||
## `createBinaryStreamHandler(onCreated, inGroup, meta?)` (function in `jazz-react`)
|
||||
|
||||
```typescript
|
||||
export function createBinaryStreamHandler(onCreated: (createdStream: C) => void, inGroup: Group, meta: C["meta"]): (event: ChangeEvent) => void
|
||||
```
|
||||
TODO: document
|
||||
|
||||
TODO: doc generator not implemented yet
|
||||
|
||||
----
|
||||
|
||||
## `createInviteLink(value, role, {baseURL?})` (function in `jazz-react`)
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,16 +16,16 @@
|
||||
"@types/qrcode": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "^0.1.14",
|
||||
"jazz-react-auth-local": "^0.1.14",
|
||||
"jazz-react": "^0.2.0",
|
||||
"jazz-react-auth-local": "^0.2.0",
|
||||
"jazz-react-media-images": "^0.2.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uniqolor": "^1.1.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
"uniqolor": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CoMap, CoID, BinaryCoStream, CoStream } from "cojson";
|
||||
import { CoMap, CoID, CoStream, Media } from "cojson";
|
||||
|
||||
/** Walkthrough: Defining the data model with CoJSON
|
||||
*
|
||||
@@ -9,18 +9,20 @@ import { CoMap, CoID, BinaryCoStream, CoStream } from "cojson";
|
||||
|
||||
export type PetPost = CoMap<{
|
||||
name: string;
|
||||
image: CoID<BinaryCoStream>;
|
||||
image: CoID<Media.ImageDefinition>;
|
||||
reactions: CoID<PetReactions>;
|
||||
}>;
|
||||
|
||||
export type ReactionType =
|
||||
| "aww"
|
||||
| "love"
|
||||
| "haha"
|
||||
| "wow"
|
||||
| "tiny"
|
||||
| "chonkers"
|
||||
| "good";
|
||||
export const REACTION_TYPES = [
|
||||
"aww",
|
||||
"love",
|
||||
"haha",
|
||||
"wow",
|
||||
"tiny",
|
||||
"chonkers",
|
||||
] as const;
|
||||
|
||||
export type ReactionType = (typeof REACTION_TYPES)[number];
|
||||
|
||||
export type PetReactions = CoStream<ReactionType>;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useJazz } from "jazz-react";
|
||||
|
||||
import { PetPost } from "./1_types";
|
||||
@@ -7,8 +5,8 @@ import { PetPost } from "./1_types";
|
||||
import { Button } from "./basicComponents";
|
||||
|
||||
import { useSimpleHashRouterThatAcceptsInvites } from "./router";
|
||||
import { PetPostUI } from "./4_PetPostUI";
|
||||
import { CreatePetPostForm } from "./4_CreatePetPostForm";
|
||||
import { RatePetPostUI } from "./4_RatePetPostUI";
|
||||
import { CreatePetPostForm } from "./3_CreatePetPostForm";
|
||||
|
||||
/** Walkthrough: Creating pet posts & routing in `<App/>`
|
||||
*
|
||||
@@ -30,7 +28,7 @@ export default function App() {
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center justify-start gap-10 pt-10 pb-10 px-5">
|
||||
{currentPetPostID ? (
|
||||
<PetPostUI petPostID={currentPetPostID} />
|
||||
<RatePetPostUI petPostID={currentPetPostID} />
|
||||
) : (
|
||||
<CreatePetPostForm onCreate={navigateToPetPostID} />
|
||||
)}
|
||||
@@ -47,4 +45,4 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Walkthrough: continue with ./3_TodoTable.tsx */
|
||||
/** Walkthrough: continue with ./3_CreatePetPostForm.tsx */
|
||||
|
||||
103
examples/pets/src/3_CreatePetPostForm.tsx
Normal file
103
examples/pets/src/3_CreatePetPostForm.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ChangeEvent, useCallback, useState } from "react";
|
||||
|
||||
import { CoID } from "cojson";
|
||||
import { useJazz, useTelepathicState } from "jazz-react";
|
||||
import { createImage } from "jazz-browser-media-images";
|
||||
|
||||
import { PetPost, PetReactions } from "./1_types";
|
||||
|
||||
import { Input, Button } from "./basicComponents";
|
||||
import { useLoadImage } from "jazz-react-media-images";
|
||||
|
||||
/** Walkthrough: TODO
|
||||
*/
|
||||
|
||||
export function CreatePetPostForm({
|
||||
onCreate,
|
||||
}: {
|
||||
onCreate: (id: CoID<PetPost>) => void;
|
||||
}) {
|
||||
const { localNode } = useJazz();
|
||||
|
||||
const [newPostId, setNewPostId] = useState<CoID<PetPost> | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const newPetPost = useTelepathicState(newPostId);
|
||||
|
||||
const onChangeName = useCallback(
|
||||
(name: string) => {
|
||||
let petPost = newPetPost;
|
||||
if (!petPost) {
|
||||
const petPostGroup = localNode.createGroup();
|
||||
petPost = petPostGroup.createMap<PetPost>();
|
||||
const petReactions = petPostGroup.createStream<PetReactions>();
|
||||
|
||||
petPost = petPost.edit((petPost) => {
|
||||
petPost.set("reactions", petReactions.id);
|
||||
});
|
||||
|
||||
setNewPostId(petPost.id);
|
||||
}
|
||||
|
||||
petPost.edit((petPost) => {
|
||||
petPost.set("name", name);
|
||||
});
|
||||
},
|
||||
[localNode, newPetPost]
|
||||
);
|
||||
|
||||
const onImageSelected = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!newPetPost || !event.target.files) return;
|
||||
|
||||
const imageDefinition = await createImage(
|
||||
event.target.files[0],
|
||||
newPetPost.group
|
||||
);
|
||||
|
||||
newPetPost.edit((petPost) => {
|
||||
petPost.set("image", imageDefinition.id);
|
||||
});
|
||||
},
|
||||
[newPetPost]
|
||||
);
|
||||
|
||||
const petImage = useLoadImage(newPetPost?.get("image"));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<p>Share your pet with friends!</p>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Pet Name"
|
||||
className="text-3xl py-6"
|
||||
onChange={(event) => onChangeName(event.target.value)}
|
||||
value={newPetPost?.get("name") || ""}
|
||||
/>
|
||||
|
||||
{petImage ? (
|
||||
<img
|
||||
className="w-80 max-w-full rounded"
|
||||
src={petImage.highestResSrc || petImage.placeholderDataURL}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type="file"
|
||||
disabled={!newPetPost?.get("name")}
|
||||
onChange={onImageSelected}
|
||||
/>
|
||||
)}
|
||||
|
||||
{newPetPost?.get("name") && newPetPost?.get("image") && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCreate(newPetPost.id);
|
||||
}}
|
||||
>
|
||||
Submit Post
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { BinaryCoStream, CoID } from "cojson";
|
||||
import {
|
||||
useBinaryStream,
|
||||
useJazz,
|
||||
useTelepathicState,
|
||||
} from "jazz-react";
|
||||
|
||||
import { PetPost, PetReactions, ReactionType } from "./1_types";
|
||||
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
} from "./basicComponents";
|
||||
|
||||
import { InviteButton } from "./components/InviteButton";
|
||||
import { NameBadge } from "./components/NameBadge";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { createBinaryStreamHandler } from "jazz-react";
|
||||
|
||||
/** Walkthrough: TODO
|
||||
*/
|
||||
|
||||
export function CreatePetPostForm({
|
||||
onCreate,
|
||||
}: {
|
||||
onCreate: (id: CoID<PetPost>) => void;
|
||||
}) {
|
||||
const { localNode } = useJazz();
|
||||
|
||||
const [creatingPostId, setCreatingPostId] = useState<
|
||||
CoID<PetPost> | undefined
|
||||
>(undefined);
|
||||
|
||||
const creatingPetPost = useTelepathicState(creatingPostId);
|
||||
|
||||
const onChangeName = useDebouncedCallback((name: string) => {
|
||||
let petPost = creatingPetPost;
|
||||
if (!petPost) {
|
||||
const petPostGroup = localNode.createGroup();
|
||||
petPost = petPostGroup.createMap<PetPost>();
|
||||
const reactions = petPostGroup.createStream<PetReactions>();
|
||||
|
||||
petPost = petPost.edit((petPost) => {
|
||||
petPost.set("reactions", reactions.id);
|
||||
});
|
||||
|
||||
setCreatingPostId(petPost.id);
|
||||
}
|
||||
|
||||
petPost.edit((petPost) => {
|
||||
petPost.set("name", name);
|
||||
});
|
||||
}, 200);
|
||||
|
||||
const onImageCreated = useCallback(
|
||||
(image: BinaryCoStream) => {
|
||||
if (!creatingPetPost) throw new Error("Never get here");
|
||||
creatingPetPost.edit((petPost) => {
|
||||
petPost.set("image", image.id);
|
||||
});
|
||||
},
|
||||
[creatingPetPost]
|
||||
);
|
||||
|
||||
const image = useBinaryStream(creatingPetPost?.get("image"));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Pet Name"
|
||||
onChange={event => onChangeName(event.target.value)}
|
||||
value={creatingPetPost?.get("name")}
|
||||
/>
|
||||
|
||||
{image ? (
|
||||
<img src={image.blobURL} />
|
||||
) : (
|
||||
creatingPetPost && (
|
||||
<Input
|
||||
type="file"
|
||||
onChange={createBinaryStreamHandler(
|
||||
onImageCreated,
|
||||
creatingPetPost.group
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{creatingPetPost?.get("name") && creatingPetPost?.get("image") && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCreate(creatingPetPost.id);
|
||||
}}
|
||||
>
|
||||
Submit Post
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { CoID } from "cojson";
|
||||
import { useTelepathicState } from "jazz-react";
|
||||
|
||||
import { PetPost } from "./1_types";
|
||||
|
||||
import { InviteButton } from "./components/InviteButton";
|
||||
import { NameBadge } from "./components/NameBadge";
|
||||
|
||||
/** Walkthrough: TODO
|
||||
*/
|
||||
|
||||
export function PetPostUI({ petPostID }: { petPostID: CoID<PetPost> }) {
|
||||
|
||||
|
||||
return (<div>TODO</div>);
|
||||
}
|
||||
103
examples/pets/src/4_RatePetPostUI.tsx
Normal file
103
examples/pets/src/4_RatePetPostUI.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { AccountID, CoID } from "cojson";
|
||||
import { useTelepathicState } from "jazz-react";
|
||||
|
||||
import { PetPost, PetReactions, ReactionType, REACTION_TYPES } from "./1_types";
|
||||
|
||||
import { ShareButton } from "./components/ShareButton";
|
||||
import { NameBadge } from "./components/NameBadge";
|
||||
import { Button } from "./basicComponents";
|
||||
import { useLoadImage } from "jazz-react-media-images";
|
||||
|
||||
/** Walkthrough: TODO
|
||||
*/
|
||||
|
||||
const reactionEmojiMap: { [reaction in ReactionType]: string } = {
|
||||
aww: "😍",
|
||||
love: "❤️",
|
||||
haha: "😂",
|
||||
wow: "😮",
|
||||
tiny: "🐥",
|
||||
chonkers: "🐘",
|
||||
};
|
||||
|
||||
export function RatePetPostUI({ petPostID }: { petPostID: CoID<PetPost> }) {
|
||||
const petPost = useTelepathicState(petPostID);
|
||||
const petReactions = useTelepathicState(petPost?.get("reactions"));
|
||||
const petImage = useLoadImage(petPost?.get("image"));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-3xl font-bold">{petPost?.get("name")}</h1>
|
||||
<ShareButton petPost={petPost} />
|
||||
</div>
|
||||
|
||||
{petImage && (
|
||||
<img
|
||||
className="w-80 max-w-full rounded"
|
||||
src={petImage.highestResSrc || petImage.placeholderDataURL}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between max-w-xs flex-wrap">
|
||||
{REACTION_TYPES.map((reactionType) => (
|
||||
<Button
|
||||
key={reactionType}
|
||||
variant={
|
||||
petReactions?.getLastItemFromMe() === reactionType
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => {
|
||||
petReactions?.edit((reactions) => {
|
||||
reactions.push(reactionType);
|
||||
});
|
||||
}}
|
||||
title={`React with ${reactionType}`}
|
||||
className="text-2xl px-2"
|
||||
>
|
||||
{reactionEmojiMap[reactionType]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{petPost?.group.myRole() === "admin" && petReactions && (
|
||||
<ReactionOverview petReactions={petReactions} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReactionOverview({ petReactions }: { petReactions: PetReactions }) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Reactions</h2>
|
||||
<div className="flex flex-col gap-1">
|
||||
{REACTION_TYPES.map((reactionType) => {
|
||||
const accountsWithThisReaction = Object.entries(
|
||||
petReactions.getLastItemsPerAccount()
|
||||
).flatMap(([accountID, reaction]) =>
|
||||
reaction === reactionType ? [accountID] : []
|
||||
);
|
||||
|
||||
if (accountsWithThisReaction.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 items-center"
|
||||
key={reactionType}
|
||||
>
|
||||
{reactionEmojiMap[reactionType]}{" "}
|
||||
{accountsWithThisReaction.map((accountID) => (
|
||||
<NameBadge
|
||||
key={accountID}
|
||||
accountID={accountID as AccountID}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
export function TitleAndLogo({name}: {name: string}) {
|
||||
return <>
|
||||
<div className="flex items-center gap-2 justify-center mt-5">
|
||||
import { Toaster } from ".";
|
||||
|
||||
export function TitleAndLogo({ name }: { name: string }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 justify-center mt-5">
|
||||
<img src="jazz-logo.png" className="h-5" /> {name}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { Button } from "./ui/button";
|
||||
export { Input } from "./ui/input";
|
||||
export { Toaster } from "./ui/toaster";
|
||||
export { useToast } from "./ui/use-toast";
|
||||
export { Skeleton } from "./ui/skeleton";
|
||||
export { TitleAndLogo } from "./TitleAndLogo";
|
||||
export { ThemeProvider } from "./themeProvider";
|
||||
|
||||
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
15
examples/pets/src/basicComponents/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
127
examples/pets/src/basicComponents/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/basicComponents/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
33
examples/pets/src/basicComponents/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/basicComponents/ui/toast"
|
||||
import { useToast } from "@/basicComponents/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
192
examples/pets/src/basicComponents/ui/use-toast.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/basicComponents/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
@@ -1,27 +1,27 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TodoProject } from "../1_types";
|
||||
import { PetPost } from "../1_types";
|
||||
|
||||
import { createInviteLink } from "jazz-react";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { useToast, Button } from "../basicComponents";
|
||||
|
||||
export function InviteButton({ list }: { list?: TodoProject }) {
|
||||
export function ShareButton({ petPost }: { petPost?: PetPost }) {
|
||||
const [existingInviteLink, setExistingInviteLink] = useState<string>();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
list?.group.myRole() === "admin" && (
|
||||
petPost?.group.myRole() === "admin" && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="py-0"
|
||||
disabled={!list}
|
||||
disabled={!petPost}
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
let inviteLink = existingInviteLink;
|
||||
if (list && !inviteLink) {
|
||||
inviteLink = createInviteLink(list, "writer");
|
||||
if (petPost && !inviteLink) {
|
||||
inviteLink = createInviteLink(petPost, "writer");
|
||||
setExistingInviteLink(inviteLink);
|
||||
}
|
||||
if (inviteLink) {
|
||||
@@ -39,7 +39,7 @@ export function InviteButton({ list }: { list?: TodoProject }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
Share
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.28",
|
||||
"version": "0.0.29",
|
||||
"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.14",
|
||||
"jazz-react-auth-local": "^0.1.14",
|
||||
"jazz-react": "^0.2.0",
|
||||
"jazz-react-auth-local": "^0.2.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.13",
|
||||
"version": "0.2.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/ws": "^8.5.5",
|
||||
@@ -16,8 +16,8 @@
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.12",
|
||||
"cojson-storage-sqlite": "^0.1.10",
|
||||
"cojson": "^0.2.0",
|
||||
"cojson-storage-sqlite": "^0.2.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.1.10",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "^0.1.12",
|
||||
"cojson": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -217,7 +217,9 @@ export class SQLiteStorage {
|
||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
||||
sessionEntry.newTransactions.flatMap((tx) => {
|
||||
if (tx.privacy !== "trusting") return [];
|
||||
return tx.changes
|
||||
// TODO: avoid parsing here?
|
||||
return cojsonInternals
|
||||
.parseJSON(tx.changes)
|
||||
.map(
|
||||
(change) =>
|
||||
change &&
|
||||
@@ -338,7 +340,7 @@ export class SQLiteStorage {
|
||||
lastSignature: msg.new[sessionID]!.lastSignature,
|
||||
};
|
||||
|
||||
const upsertedSession = (this.db
|
||||
const upsertedSession = this.db
|
||||
.prepare<[number, string, number, string]>(
|
||||
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature
|
||||
@@ -349,7 +351,7 @@ export class SQLiteStorage {
|
||||
sessionUpdate.sessionID,
|
||||
sessionUpdate.lastIdx,
|
||||
sessionUpdate.lastSignature
|
||||
) as {rowID: number});
|
||||
) as { rowID: number };
|
||||
|
||||
const sessionRowID = upsertedSession.rowID;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.1.12",
|
||||
"version": "0.2.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface CoValue {
|
||||
export interface WriteableCoValue extends CoValue {}
|
||||
|
||||
export type CoValueImpl =
|
||||
| CoMap<{ [key: string]: JsonValue }, JsonObject | null>
|
||||
| CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>
|
||||
| CoList<JsonValue, JsonObject | null>
|
||||
| CoStream<JsonValue, JsonObject | null>
|
||||
| BinaryCoStream<BinaryCoStreamMeta>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
||||
import { MapOpPayload } from "./coValues/coMap.js";
|
||||
import { Role } from "./permissions.js";
|
||||
import { cojsonReady } from "./index.js";
|
||||
import { stableStringify } from "./jsonStringify.js";
|
||||
|
||||
beforeEach(async () => {
|
||||
await cojsonReady;
|
||||
@@ -24,11 +25,11 @@ test("Can create coValue with new agent credentials and add transaction to it",
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
@@ -61,11 +62,11 @@ test("transactions with wrong signature are rejected", () => {
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
@@ -97,11 +98,11 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
const transaction: Transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "world",
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
const { expectedNewHash } = coValue.expectedNewHashAfter(
|
||||
@@ -110,11 +111,11 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
|
||||
{
|
||||
privacy: "trusting",
|
||||
madeAt: Date.now(),
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
hello: "wrong",
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -154,13 +155,13 @@ test("New transactions in a group correctly update owned values, including subsc
|
||||
const resignationThatWeJustLearnedAbout = {
|
||||
privacy: "trusting",
|
||||
madeAt: timeBeforeEdit,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: account.id,
|
||||
value: "revoked"
|
||||
} satisfies MapOpPayload<typeof account.id, Role>
|
||||
]
|
||||
])
|
||||
} satisfies Transaction;
|
||||
|
||||
const { expectedNewHash } = group.underlyingMap.core.expectedNewHashAfter(sessionID, [
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
decryptKeySecret,
|
||||
getAgentSignerID,
|
||||
getAgentSealerID,
|
||||
decryptRawForTransaction,
|
||||
} from "./crypto.js";
|
||||
import { JsonObject, JsonValue } from "./jsonValue.js";
|
||||
import { base58 } from "@scure/base";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
AccountID,
|
||||
GeneralizedControlledAccount,
|
||||
} from "./account.js";
|
||||
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
|
||||
|
||||
export type CoValueHeader = {
|
||||
type: CoValueImpl["type"];
|
||||
@@ -77,17 +79,18 @@ export type PrivateTransaction = {
|
||||
>;
|
||||
};
|
||||
|
||||
|
||||
export type TrustingTransaction = {
|
||||
privacy: "trusting";
|
||||
madeAt: number;
|
||||
changes: JsonValue[];
|
||||
changes: Stringified<JsonValue[]>;
|
||||
};
|
||||
|
||||
export type Transaction = PrivateTransaction | TrustingTransaction;
|
||||
|
||||
export type DecryptedTransaction = {
|
||||
txID: TransactionID;
|
||||
changes: JsonValue[];
|
||||
changes: Stringified<JsonValue[]>;
|
||||
madeAt: number;
|
||||
};
|
||||
|
||||
@@ -100,7 +103,7 @@ export class CoValueCore {
|
||||
_sessions: { [key: SessionID]: SessionLog };
|
||||
_cachedContent?: CoValueImpl;
|
||||
listeners: Set<(content?: CoValueImpl) => void> = new Set();
|
||||
_decryptionCache: {[key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined} = {}
|
||||
_decryptionCache: {[key: Encrypted<JsonValue[], JsonValue>]: Stringified<JsonValue[]> | undefined} = {}
|
||||
|
||||
constructor(
|
||||
header: CoValueHeader,
|
||||
@@ -415,7 +418,7 @@ export class CoValueCore {
|
||||
tx: this.nextTransactionID(),
|
||||
});
|
||||
|
||||
this._decryptionCache[encrypted] = changes;
|
||||
this._decryptionCache[encrypted] = stableStringify(changes);
|
||||
|
||||
transaction = {
|
||||
privacy: "private",
|
||||
@@ -427,7 +430,7 @@ export class CoValueCore {
|
||||
transaction = {
|
||||
privacy: "trusting",
|
||||
madeAt,
|
||||
changes,
|
||||
changes: stableStringify(changes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -500,7 +503,7 @@ export class CoValueCore {
|
||||
let decrytedChanges = this._decryptionCache[tx.encryptedChanges];
|
||||
|
||||
if (!decrytedChanges) {
|
||||
decrytedChanges = decryptForTransaction(
|
||||
decrytedChanges = decryptRawForTransaction(
|
||||
tx.encryptedChanges,
|
||||
readKey,
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { SessionID, TransactionID } from "../ids.js";
|
||||
import { Group } from "../group.js";
|
||||
import { AccountID, isAccountID } from "../account.js";
|
||||
import { parseJSON } from "../jsonStringify.js";
|
||||
|
||||
type OpID = TransactionID & { changeIdx: number };
|
||||
|
||||
@@ -98,7 +99,7 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
|
||||
changes,
|
||||
madeAt,
|
||||
} of this.core.getValidSortedTransactions()) {
|
||||
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
||||
for (const [changeIdx, changeUntyped] of parseJSON(changes).entries()) {
|
||||
const change = changeUntyped as ListOpPayload<T>;
|
||||
|
||||
if (change.op === "pre" || change.op === "app") {
|
||||
|
||||
@@ -4,15 +4,16 @@ import { CoID, ReadableCoValue, WriteableCoValue } from '../coValue.js';
|
||||
import { CoValueCore, accountOrAgentIDfromSessionID } from '../coValueCore.js';
|
||||
import { AccountID, isAccountID } from '../account.js';
|
||||
import { Group } from '../group.js';
|
||||
import { parseJSON } from '../jsonStringify.js';
|
||||
|
||||
type MapOp<K extends string, V extends JsonValue> = {
|
||||
type MapOp<K extends string, V extends JsonValue | undefined> = {
|
||||
txID: TransactionID;
|
||||
madeAt: number;
|
||||
changeIdx: number;
|
||||
} & MapOpPayload<K, V>;
|
||||
// TODO: add after TransactionID[] for conflicts/ordering
|
||||
|
||||
export type MapOpPayload<K extends string, V extends JsonValue> = {
|
||||
export type MapOpPayload<K extends string, V extends JsonValue | undefined> = {
|
||||
op: "set";
|
||||
key: K;
|
||||
value: V;
|
||||
@@ -22,18 +23,16 @@ export type MapOpPayload<K extends string, V extends JsonValue> = {
|
||||
key: K;
|
||||
};
|
||||
|
||||
export type MapK<M extends { [key: string]: JsonValue; }> = keyof M & string;
|
||||
export type MapV<M extends { [key: string]: JsonValue; }> = M[MapK<M>];
|
||||
export type MapM<M extends { [key: string]: JsonValue; }> = {
|
||||
[KK in MapK<M>]: M[KK];
|
||||
}
|
||||
export type MapK<M extends { [key: string]: JsonValue | undefined; }> = keyof M & string;
|
||||
export type MapV<M extends { [key: string]: JsonValue | undefined; }> = M[MapK<M>];
|
||||
|
||||
|
||||
/** A collaborative map with precise shape `M` and optional static metadata `Meta` */
|
||||
export class CoMap<
|
||||
M extends { [key: string]: JsonValue; },
|
||||
M extends { [key: string]: JsonValue | undefined; },
|
||||
Meta extends JsonObject | null = null,
|
||||
> implements ReadableCoValue {
|
||||
id: CoID<CoMap<MapM<M>, Meta>>;
|
||||
id: CoID<CoMap<M, Meta>>;
|
||||
type = "comap" as const;
|
||||
core: CoValueCore;
|
||||
/** @internal */
|
||||
@@ -43,7 +42,7 @@ export class CoMap<
|
||||
|
||||
/** @internal */
|
||||
constructor(core: CoValueCore) {
|
||||
this.id = core.id as CoID<CoMap<MapM<M>, Meta>>;
|
||||
this.id = core.id as CoID<CoMap<M, Meta>>;
|
||||
this.core = core;
|
||||
this.ops = {};
|
||||
|
||||
@@ -64,7 +63,7 @@ export class CoMap<
|
||||
|
||||
for (const { txID, changes, madeAt } of this.core.getValidSortedTransactions()) {
|
||||
for (const [changeIdx, changeUntyped] of (
|
||||
changes
|
||||
parseJSON(changes)
|
||||
).entries()) {
|
||||
const change = changeUntyped as MapOpPayload<MapK<M>, MapV<M>>;
|
||||
let entries = this.ops[change.key];
|
||||
@@ -207,7 +206,7 @@ export class CoMap<
|
||||
}
|
||||
|
||||
export class WriteableCoMap<
|
||||
M extends { [key: string]: JsonValue; },
|
||||
M extends { [key: string]: JsonValue | undefined; },
|
||||
Meta extends JsonObject | null = null,
|
||||
> extends CoMap<M, Meta> implements WriteableCoValue {
|
||||
/** @internal */
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||
import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
|
||||
import { CoValueCore } from "../coValueCore.js";
|
||||
import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
|
||||
import { Group } from "../group.js";
|
||||
import { SessionID } from "../ids.js";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
||||
import { AccountID } from "../index.js";
|
||||
import { isAccountID } from "../account.js";
|
||||
import { parseJSON } from "../jsonStringify.js";
|
||||
|
||||
export type BinaryChunkInfo = {
|
||||
mimeType: string;
|
||||
@@ -40,7 +43,7 @@ export class CoStream<
|
||||
type = "costream" as const;
|
||||
core: CoValueCore;
|
||||
items: {
|
||||
[key: SessionID]: T[];
|
||||
[key: SessionID]: {item: T, madeAt: number}[];
|
||||
};
|
||||
|
||||
constructor(core: CoValueCore) {
|
||||
@@ -64,16 +67,17 @@ export class CoStream<
|
||||
|
||||
for (const {
|
||||
txID,
|
||||
madeAt,
|
||||
changes,
|
||||
} of this.core.getValidSortedTransactions()) {
|
||||
for (const changeUntyped of changes) {
|
||||
for (const changeUntyped of parseJSON(changes)) {
|
||||
const change = changeUntyped as T;
|
||||
let entries = this.items[txID.sessionID];
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
this.items[txID.sessionID] = entries;
|
||||
}
|
||||
entries.push(change);
|
||||
entries.push({item: change, madeAt});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,13 +91,57 @@ export class CoStream<
|
||||
);
|
||||
}
|
||||
|
||||
return Object.values(this.items)[0];
|
||||
return Object.values(this.items)[0]?.map(item => item.item);
|
||||
}
|
||||
|
||||
getLastItemsPerAccount(): {[account: AccountID]: T | undefined} {
|
||||
const result: {[account: AccountID]: {item: T, madeAt: number} | undefined} = {};
|
||||
|
||||
for (const [sessionID, items] of Object.entries(this.items)) {
|
||||
const account = accountOrAgentIDfromSessionID(sessionID as SessionID);
|
||||
if (!isAccountID(account)) continue;
|
||||
if (items.length > 0) {
|
||||
const lastItemOfSession = items[items.length - 1]!;
|
||||
if (!result[account] || lastItemOfSession.madeAt > result[account]!.madeAt) {
|
||||
result[account] = lastItemOfSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(Object.entries(result).map(([account, item]) =>
|
||||
[account, item?.item]
|
||||
));
|
||||
}
|
||||
|
||||
getLastItemFrom(account: AccountID): T | undefined {
|
||||
let lastItem: {item: T, madeAt: number} | undefined;
|
||||
|
||||
for (const [sessionID, items] of Object.entries(this.items)) {
|
||||
if (sessionID.startsWith(account)) {
|
||||
if (items.length > 0) {
|
||||
const lastItemOfSession = items[items.length - 1]!;
|
||||
if (!lastItem || lastItemOfSession.madeAt > lastItem.madeAt) {
|
||||
lastItem = lastItemOfSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastItem?.item;
|
||||
}
|
||||
|
||||
getLastItemFromMe(): T | undefined {
|
||||
const myAccountID = this.core.node.account.id;
|
||||
if (!isAccountID(myAccountID)) return undefined;
|
||||
return this.getLastItemFrom(myAccountID);
|
||||
}
|
||||
|
||||
toJSON(): {
|
||||
[key: SessionID]: T[];
|
||||
} {
|
||||
return this.items;
|
||||
return Object.fromEntries(Object.entries(this.items).map(([sessionID, items]) =>
|
||||
[sessionID, items.map(item => item.item)]
|
||||
));
|
||||
}
|
||||
|
||||
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AgentID, RawCoID, TransactionID } from "./ids.js";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
||||
|
||||
import { createBLAKE3 } from 'hash-wasm';
|
||||
import { stableStringify } from "./fastJsonStableStringify.js";
|
||||
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
|
||||
|
||||
let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
|
||||
let blake3HashOnce: (data: Uint8Array) => Uint8Array;
|
||||
@@ -316,11 +316,11 @@ export function encryptKeySecret(keys: {
|
||||
};
|
||||
}
|
||||
|
||||
function decrypt<T extends JsonValue, N extends JsonValue>(
|
||||
function decryptRaw<T extends JsonValue, N extends JsonValue>(
|
||||
encrypted: Encrypted<T, N>,
|
||||
keySecret: KeySecret,
|
||||
nOnceMaterial: N
|
||||
): T | undefined {
|
||||
): Stringified<T> {
|
||||
const keySecretBytes = base58.decode(
|
||||
keySecret.substring("keySecret_z".length)
|
||||
);
|
||||
@@ -333,13 +333,31 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
|
||||
);
|
||||
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
||||
|
||||
return textDecoder.decode(plaintext) as Stringified<T>;
|
||||
|
||||
}
|
||||
|
||||
function decrypt<T extends JsonValue, N extends JsonValue>(
|
||||
encrypted: Encrypted<T, N>,
|
||||
keySecret: KeySecret,
|
||||
nOnceMaterial: N
|
||||
): T | undefined {
|
||||
try {
|
||||
return JSON.parse(textDecoder.decode(plaintext));
|
||||
return parseJSON(decryptRaw(encrypted, keySecret, nOnceMaterial));
|
||||
} catch (e) {
|
||||
console.error("Decryption error", e)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptRawForTransaction<T extends JsonValue>(
|
||||
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
||||
keySecret: KeySecret,
|
||||
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
||||
): Stringified<T> | undefined {
|
||||
return decryptRaw(encrypted, keySecret, nOnceMaterial);
|
||||
}
|
||||
|
||||
export function decryptForTransaction<T extends JsonValue>(
|
||||
encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
|
||||
keySecret: KeySecret,
|
||||
|
||||
@@ -238,7 +238,7 @@ export class Group {
|
||||
|
||||
/** Creates a new `CoMap` within this group, with the specified specialized
|
||||
* `CoMap` type `M` and optional static metadata. */
|
||||
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
||||
createMap<M extends CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>>(
|
||||
meta?: M["meta"]
|
||||
): M {
|
||||
return this.node
|
||||
|
||||
@@ -25,6 +25,7 @@ import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
||||
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
||||
import { Group, expectGroupContent } from "./group.js";
|
||||
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
||||
import { parseJSON } from "./jsonStringify.js";
|
||||
|
||||
import type { SessionID, AgentID } from "./ids.js";
|
||||
import type { CoID, CoValueImpl } from "./coValue.js";
|
||||
@@ -34,6 +35,7 @@ import type { SyncMessage, Peer } from "./sync.js";
|
||||
import type { AgentSecret } from "./crypto.js";
|
||||
import type { AccountID, Profile } from "./account.js";
|
||||
import type { InviteSecret } from "./group.js";
|
||||
import type * as Media from "./media.js";
|
||||
|
||||
type Value = JsonValue | CoValueImpl;
|
||||
|
||||
@@ -53,7 +55,8 @@ export const cojsonInternals = {
|
||||
shortHashLength,
|
||||
expectGroupContent,
|
||||
base64URLtoBytes,
|
||||
bytesToBase64url
|
||||
bytesToBase64url,
|
||||
parseJSON
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -90,6 +93,7 @@ export type {
|
||||
AgentSecret,
|
||||
InviteSecret,
|
||||
SyncMessage,
|
||||
Media
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
// adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function stableStringify(data: any): string | undefined {
|
||||
export type Stringified<T> = string & { __type: T };
|
||||
|
||||
export function stableStringify<T>(data: T): Stringified<T>
|
||||
export function stableStringify(data: undefined): undefined
|
||||
export function stableStringify<T>(data: T | undefined): Stringified<T> | undefined {
|
||||
const cycles = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const seen: any[] = [];
|
||||
let node = data;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let node = data as any;
|
||||
|
||||
if (node && node.toJSON && typeof node.toJSON === "function") {
|
||||
node = node.toJSON();
|
||||
}
|
||||
|
||||
if (node === undefined) return;
|
||||
if (typeof node == "number") return isFinite(node) ? "" + node : "null";
|
||||
if (typeof node == "number")
|
||||
return (isFinite(node) ? "" + node : "null") as Stringified<T>;
|
||||
if (typeof node !== "object") {
|
||||
if (typeof node === "string" && (node.startsWith("encrypted_U") || node.startsWith("binary_U"))) {
|
||||
return `"${node}"`;
|
||||
if (
|
||||
typeof node === "string" &&
|
||||
(node.startsWith("encrypted_U") || node.startsWith("binary_U"))
|
||||
) {
|
||||
return `"${node}"` as Stringified<T>;
|
||||
}
|
||||
return JSON.stringify(node);
|
||||
return JSON.stringify(node) as Stringified<T>;
|
||||
}
|
||||
|
||||
let i, out;
|
||||
@@ -28,13 +36,13 @@ export function stableStringify(data: any): string | undefined {
|
||||
if (i) out += ",";
|
||||
out += stableStringify(node[i]) || "null";
|
||||
}
|
||||
return out + "]";
|
||||
return (out + "]") as Stringified<T>;
|
||||
}
|
||||
|
||||
if (node === null) return "null";
|
||||
if (node === null) return "null" as Stringified<T>;
|
||||
|
||||
if (seen.indexOf(node) !== -1) {
|
||||
if (cycles) return JSON.stringify("__cycle__");
|
||||
if (cycles) return JSON.stringify("__cycle__") as Stringified<T>;
|
||||
throw new TypeError("Converting circular structure to JSON");
|
||||
}
|
||||
|
||||
@@ -50,5 +58,9 @@ export function stableStringify(data: any): string | undefined {
|
||||
out += JSON.stringify(key) + ":" + value;
|
||||
}
|
||||
seen.splice(seenIndex, 1);
|
||||
return "{" + out + "}";
|
||||
return ("{" + out + "}") as Stringified<T>;
|
||||
}
|
||||
|
||||
export function parseJSON<T>(json: Stringified<T>): T {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
@@ -3,4 +3,4 @@ import { RawCoID } from './ids.js';
|
||||
export type JsonAtom = string | number | boolean | null;
|
||||
export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
|
||||
export type JsonArray = JsonValue[];
|
||||
export type JsonObject = { [key: string]: JsonValue; };
|
||||
export type JsonObject = { [key: string]: JsonValue | undefined; };
|
||||
|
||||
9
packages/cojson/src/media.ts
Normal file
9
packages/cojson/src/media.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CoMap } from './coValues/coMap.js'
|
||||
import { CoID } from './coValue.js'
|
||||
import { BinaryCoStream } from './coValues/coStream.js'
|
||||
|
||||
export type ImageDefinition = CoMap<{
|
||||
originalSize: [number, number];
|
||||
placeholderDataURL?: string;
|
||||
[res: `${number}x${number}`]: CoID<BinaryCoStream>;
|
||||
}>;
|
||||
@@ -208,7 +208,7 @@ export class LocalNode {
|
||||
reject(
|
||||
new Error("Couldn't find invite before timeout")
|
||||
),
|
||||
1000
|
||||
2000
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
AccountID,
|
||||
Profile,
|
||||
} from "./account.js";
|
||||
import { parseJSON } from "./jsonStringify.js";
|
||||
|
||||
export type PermissionsDef =
|
||||
| { type: "group"; initialAdmin: AccountID | AgentID }
|
||||
@@ -76,11 +77,13 @@ export function determineValidTransactions(
|
||||
// console.log("before", { memberState, validTransactions });
|
||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||
|
||||
const change = tx.changes[0] as
|
||||
const changes = parseJSON(tx.changes)
|
||||
|
||||
const change = changes[0] as
|
||||
| MapOpPayload<AccountID | AgentID, Role>
|
||||
| MapOpPayload<"readKey", JsonValue>
|
||||
| MapOpPayload<"profile", CoID<Profile>>;
|
||||
if (tx.changes.length !== 1) {
|
||||
if (changes.length !== 1) {
|
||||
console.warn("Group transaction must have exactly one change");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ import {
|
||||
randomAnonymousAccountAndSessionID,
|
||||
shouldNotResolve,
|
||||
} from "./testUtils.js";
|
||||
import {
|
||||
connectedPeers,
|
||||
newStreamPair
|
||||
} from "./streamUtils.js";
|
||||
import { connectedPeers, newStreamPair } from "./streamUtils.js";
|
||||
import { AccountID } from "./account.js";
|
||||
import { cojsonReady } from "./index.js";
|
||||
import { stableStringify } from "./jsonStringify.js";
|
||||
|
||||
beforeEach(async () => {
|
||||
await cojsonReady;
|
||||
@@ -84,13 +82,13 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "hello",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -162,13 +160,13 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "goodbye",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -251,13 +249,13 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "hello",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -283,13 +281,13 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "goodbye",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -362,13 +360,13 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "hello",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -465,13 +463,13 @@ test("No matter the optimistic known state, node respects invalid known state me
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[1]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "goodbye",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
@@ -568,13 +566,13 @@ test("If we add a server peer, all updates to all coValues are sent to it, even
|
||||
privacy: "trusting" as const,
|
||||
madeAt: map.core.sessions[node.currentSessionID]!
|
||||
.transactions[0]!.madeAt,
|
||||
changes: [
|
||||
changes: stableStringify([
|
||||
{
|
||||
op: "set",
|
||||
key: "hello",
|
||||
value: "world",
|
||||
} satisfies MapOpPayload<string, string>,
|
||||
],
|
||||
]),
|
||||
},
|
||||
],
|
||||
lastSignature:
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
WritableStreamDefaultWriter,
|
||||
} from "isomorphic-streams";
|
||||
import { RawCoID, SessionID } from "./ids.js";
|
||||
import { stableStringify } from "./fastJsonStableStringify.js";
|
||||
import { stableStringify } from "./jsonStringify.js";
|
||||
|
||||
export type CoValueKnownState = {
|
||||
id: RawCoID;
|
||||
@@ -269,7 +269,6 @@ export class SyncManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log("DONE!!!");
|
||||
} catch (e) {
|
||||
console.error(`Error reading from peer ${peer.id}`, e);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-local",
|
||||
"version": "0.1.12",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser": "^0.1.12",
|
||||
"jazz-browser": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -128,7 +128,7 @@ async function signUp(
|
||||
},
|
||||
user: {
|
||||
id: webAuthNCredentialPayload,
|
||||
name: username + `(${new Date().toLocaleString()})`,
|
||||
name: username + ` (${new Date().toLocaleString()})`,
|
||||
displayName: username,
|
||||
},
|
||||
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
||||
|
||||
17
packages/jazz-browser-media-images/.eslintrc.cjs
Normal file
17
packages/jazz-browser-media-images/.eslintrc.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
},
|
||||
};
|
||||
171
packages/jazz-browser-media-images/.gitignore
vendored
Normal file
171
packages/jazz-browser-media-images/.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
.DS_Store
|
||||
2
packages/jazz-browser-media-images/.npmignore
Normal file
2
packages/jazz-browser-media-images/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
21
packages/jazz-browser-media-images/package.json
Normal file
21
packages/jazz-browser-media-images/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.2.0",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/image-blob-reduce": "^4.1.1"
|
||||
}
|
||||
}
|
||||
253
packages/jazz-browser-media-images/src/index.ts
Normal file
253
packages/jazz-browser-media-images/src/index.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { CoID, Group, LocalNode, Media } from "cojson";
|
||||
|
||||
import ImageBlobReduce from "image-blob-reduce";
|
||||
import Pica from "pica";
|
||||
import {
|
||||
createBinaryStreamFromBlob,
|
||||
readBlobFromBinaryStream,
|
||||
} from "jazz-browser";
|
||||
|
||||
const pica = new Pica();
|
||||
|
||||
export async function createImage(
|
||||
image: Blob | File,
|
||||
inGroup: Group
|
||||
): Promise<Media.ImageDefinition> {
|
||||
let originalWidth!: number;
|
||||
let originalHeight!: number;
|
||||
const Reducer = new ImageBlobReduce({ pica });
|
||||
Reducer.after("_blob_to_image", (env) => {
|
||||
originalWidth =
|
||||
(env as unknown as { orientation: number }).orientation & 4
|
||||
? env.image.height
|
||||
: env.image.width;
|
||||
originalHeight =
|
||||
(env as unknown as { orientation: number }).orientation & 4
|
||||
? env.image.width
|
||||
: env.image.height;
|
||||
return Promise.resolve(env);
|
||||
});
|
||||
|
||||
const placeholderDataURL = (
|
||||
await Reducer.toCanvas(image, { max: 8 })
|
||||
).toDataURL("image/png");
|
||||
|
||||
let imageDefinition = inGroup.createMap<Media.ImageDefinition>();
|
||||
|
||||
imageDefinition = imageDefinition.edit((imageDefinition) => {
|
||||
imageDefinition.set("originalSize", [originalWidth, originalHeight]);
|
||||
imageDefinition.set("placeholderDataURL", placeholderDataURL);
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
const max256 = await Reducer.toBlob(image, { max: 256 });
|
||||
|
||||
if (originalWidth > 256 || originalHeight > 256) {
|
||||
const width =
|
||||
originalWidth > originalHeight
|
||||
? 256
|
||||
: Math.round(256 * (originalWidth / originalHeight));
|
||||
const height =
|
||||
originalHeight > originalWidth
|
||||
? 256
|
||||
: Math.round(256 * (originalHeight / originalWidth));
|
||||
|
||||
const binaryStreamId = (
|
||||
await createBinaryStreamFromBlob(max256, inGroup)
|
||||
).id;
|
||||
|
||||
imageDefinition.edit((imageDefinition) => {
|
||||
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const max1024 = await Reducer.toBlob(image, { max: 1024 });
|
||||
|
||||
if (originalWidth > 1024 || originalHeight > 1024) {
|
||||
const width =
|
||||
originalWidth > originalHeight
|
||||
? 1024
|
||||
: Math.round(1024 * (originalWidth / originalHeight));
|
||||
const height =
|
||||
originalHeight > originalWidth
|
||||
? 1024
|
||||
: Math.round(1024 * (originalHeight / originalWidth));
|
||||
|
||||
const binaryStreamId = (
|
||||
await createBinaryStreamFromBlob(max1024, inGroup)
|
||||
).id;
|
||||
|
||||
imageDefinition.edit((imageDefinition) => {
|
||||
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const max2048 = await Reducer.toBlob(image, { max: 2048 });
|
||||
|
||||
if (originalWidth > 2048 || originalHeight > 2048) {
|
||||
const width =
|
||||
originalWidth > originalHeight
|
||||
? 2048
|
||||
: Math.round(2048 * (originalWidth / originalHeight));
|
||||
const height =
|
||||
originalHeight > originalWidth
|
||||
? 2048
|
||||
: Math.round(2048 * (originalHeight / originalWidth));
|
||||
|
||||
const binaryStreamId = (
|
||||
await createBinaryStreamFromBlob(max2048, inGroup)
|
||||
).id;
|
||||
|
||||
imageDefinition.edit((imageDefinition) => {
|
||||
imageDefinition.set(`${width}x${height}`, binaryStreamId);
|
||||
});
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const originalBinaryStreamId = (
|
||||
await createBinaryStreamFromBlob(image, inGroup)
|
||||
).id;
|
||||
|
||||
imageDefinition.edit((imageDefinition) => {
|
||||
imageDefinition.set(
|
||||
`${originalWidth}x${originalHeight}`,
|
||||
originalBinaryStreamId
|
||||
);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return imageDefinition;
|
||||
}
|
||||
|
||||
export type LoadingImageInfo = {
|
||||
originalSize?: [number, number];
|
||||
placeholderDataURL?: string;
|
||||
highestResSrc?: string;
|
||||
};
|
||||
|
||||
export function loadImage(
|
||||
imageID: CoID<Media.ImageDefinition>,
|
||||
localNode: LocalNode,
|
||||
progressiveCallback: (update: LoadingImageInfo) => void
|
||||
): () => void {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let stopped = false;
|
||||
|
||||
const resState: {
|
||||
[res: `${number}x${number}`]:
|
||||
| { state: "queued" }
|
||||
| { state: "loading" }
|
||||
| { state: "loaded"; blobURL: string }
|
||||
| { state: "revoked" }
|
||||
| { state: "failed" }
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
const cleanUp = () => {
|
||||
stopped = true;
|
||||
for (const [res, entry] of Object.entries(resState)) {
|
||||
if (entry?.state === "loaded") {
|
||||
URL.revokeObjectURL(entry.blobURL);
|
||||
resState[res as `${number}x${number}`] = { state: "revoked" };
|
||||
}
|
||||
}
|
||||
unsubscribe?.();
|
||||
};
|
||||
|
||||
localNode
|
||||
.load(imageID)
|
||||
.then((imageDefinition) => {
|
||||
if (stopped) return;
|
||||
unsubscribe = imageDefinition.subscribe(async (imageDefinition) => {
|
||||
if (stopped) return;
|
||||
|
||||
const originalSize = imageDefinition.get("originalSize");
|
||||
const placeholderDataURL =
|
||||
imageDefinition.get("placeholderDataURL");
|
||||
|
||||
const resolutions = imageDefinition.keys()
|
||||
.filter(
|
||||
(key): key is `${number}x${number}` =>
|
||||
!!key.match(/\d+x\d+/)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const widthA = Number(a.split("x")[0]);
|
||||
const widthB = Number(b.split("x")[0]);
|
||||
return widthA - widthB;
|
||||
});
|
||||
|
||||
const startLoading = async () => {
|
||||
|
||||
const notYetQueuedOrLoading = resolutions.filter(
|
||||
(res) => !resState[res]
|
||||
);
|
||||
|
||||
console.log("Loading iteration", resolutions, resState, notYetQueuedOrLoading);
|
||||
|
||||
for (const res of notYetQueuedOrLoading) {
|
||||
resState[res] = { state: "queued" };
|
||||
}
|
||||
|
||||
for (const res of notYetQueuedOrLoading) {
|
||||
if (stopped) return;
|
||||
resState[res] = { state: "loading" };
|
||||
|
||||
const binaryStreamId = imageDefinition.get(res)!;
|
||||
console.log("Loading image res", imageID, res, binaryStreamId);
|
||||
|
||||
const blob = await readBlobFromBinaryStream(
|
||||
binaryStreamId,
|
||||
localNode
|
||||
);
|
||||
|
||||
if (stopped) return;
|
||||
if (!blob) {
|
||||
resState[res] = { state: "failed" };
|
||||
console.log("Loading image res failed", imageID, res, binaryStreamId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
resState[res] = { state: "loaded", blobURL };
|
||||
|
||||
console.log("Loaded image res", imageID, res, binaryStreamId);
|
||||
|
||||
progressiveCallback({
|
||||
originalSize,
|
||||
placeholderDataURL,
|
||||
highestResSrc: blobURL,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
!Object.values(resState).some(
|
||||
(entry) => entry?.state === "loaded"
|
||||
)
|
||||
) {
|
||||
progressiveCallback({
|
||||
originalSize,
|
||||
placeholderDataURL,
|
||||
});
|
||||
}
|
||||
|
||||
startLoading().catch((err) => {
|
||||
console.error("Error loading image", imageID, err);
|
||||
cleanUp();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error loading image", imageID, err);
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
return cleanUp;
|
||||
}
|
||||
16
packages/jazz-browser-media-images/tsconfig.json
Normal file
16
packages/jazz-browser-media-images/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
75
packages/jazz-browser-media-images/yarn.lock
Normal file
75
packages/jazz-browser-media-images/yarn.lock
Normal file
@@ -0,0 +1,75 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@noble/ciphers@^0.1.3":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
|
||||
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
|
||||
|
||||
"@noble/curves@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.1"
|
||||
|
||||
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||
|
||||
"@scure/base@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react@^18.2.19":
|
||||
version "18.2.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
|
||||
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||
|
||||
cojson@^0.0.14:
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
|
||||
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.1.3"
|
||||
"@noble/curves" "^1.1.0"
|
||||
"@noble/hashes" "^1.3.1"
|
||||
"@scure/base" "^1.1.1"
|
||||
fast-json-stable-stringify "^2.1.0"
|
||||
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
|
||||
version "1.0.3"
|
||||
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
typescript@^5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.1.12",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.12",
|
||||
"jazz-storage-indexeddb": "^0.1.12",
|
||||
"cojson": "^0.2.0",
|
||||
"jazz-storage-indexeddb": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react-auth-local",
|
||||
"version": "0.1.14",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jazz-browser-auth-local": "^0.1.12",
|
||||
"jazz-react": "^0.1.14",
|
||||
"jazz-browser-auth-local": "^0.2.0",
|
||||
"jazz-react": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
17
packages/jazz-react-media-images/.eslintrc.cjs
Normal file
17
packages/jazz-react-media-images/.eslintrc.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
},
|
||||
};
|
||||
171
packages/jazz-react-media-images/.gitignore
vendored
Normal file
171
packages/jazz-react-media-images/.gitignore
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
.DS_Store
|
||||
2
packages/jazz-react-media-images/.npmignore
Normal file
2
packages/jazz-react-media-images/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
26
packages/jazz-react-media-images/package.json
Normal file
26
packages/jazz-react-media-images/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "jazz-react-media-images",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.2.0",
|
||||
"jazz-browser": "^0.2.0",
|
||||
"jazz-browser-media-images": "^0.2.0",
|
||||
"jazz-react": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17 - 18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.tsx",
|
||||
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13"
|
||||
}
|
||||
24
packages/jazz-react-media-images/src/index.tsx
Normal file
24
packages/jazz-react-media-images/src/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CoID, Media } from "cojson";
|
||||
import { loadImage, LoadingImageInfo } from "jazz-browser-media-images";
|
||||
import { useJazz } from "jazz-react";
|
||||
import { useEffect, useState } from "react";
|
||||
export { createImage } from "jazz-browser-media-images";
|
||||
|
||||
export function useLoadImage(
|
||||
imageID?: CoID<Media.ImageDefinition>
|
||||
): LoadingImageInfo | undefined {
|
||||
const { localNode } = useJazz();
|
||||
|
||||
const [imageInfo, setImageInfo] = useState<LoadingImageInfo>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageID) return;
|
||||
const unsubscribe = loadImage(imageID, localNode, (imageInfo) => {
|
||||
setImageInfo(imageInfo);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [imageID, localNode]);
|
||||
|
||||
return imageInfo;
|
||||
}
|
||||
16
packages/jazz-react-media-images/tsconfig.json
Normal file
16
packages/jazz-react-media-images/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
}
|
||||
75
packages/jazz-react-media-images/yarn.lock
Normal file
75
packages/jazz-react-media-images/yarn.lock
Normal file
@@ -0,0 +1,75 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@noble/ciphers@^0.1.3":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.1.4.tgz#96327dca147829ed9eee0d96cfdf7c57915765f0"
|
||||
integrity sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==
|
||||
|
||||
"@noble/curves@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
|
||||
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.1"
|
||||
|
||||
"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
|
||||
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
|
||||
|
||||
"@scure/base@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
|
||||
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react@^18.2.19":
|
||||
version "18.2.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.19.tgz#f77cb2c8307368e624d464a25b9675fa35f95a8b"
|
||||
integrity sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||
|
||||
cojson@^0.0.14:
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/cojson/-/cojson-0.0.14.tgz#e7b190ade1efc20d6f0fa12411d7208cdc0f19f7"
|
||||
integrity sha512-TFenIGswEEhnZlCmq+B1NZPztjovZ72AjK1YkkZca54ZFbB1lAHdPt2hqqu/QBO24C9+6DtuoS2ixm6gbSBWCg==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.1.3"
|
||||
"@noble/curves" "^1.1.0"
|
||||
"@noble/hashes" "^1.3.1"
|
||||
"@scure/base" "^1.1.1"
|
||||
fast-json-stable-stringify "^2.1.0"
|
||||
isomorphic-streams "https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
"isomorphic-streams@git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae":
|
||||
version "1.0.3"
|
||||
resolved "git+https://github.com/sgwilym/isomorphic-streams.git#aa9394781bfc92f8d7c981be7daf8af4b4cd4fae"
|
||||
|
||||
typescript@^5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.1.14",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.12",
|
||||
"jazz-browser": "^0.1.12",
|
||||
"cojson": "^0.2.0",
|
||||
"jazz-browser": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,14 +8,9 @@ import {
|
||||
CojsonInternalTypes,
|
||||
BinaryCoStream,
|
||||
BinaryCoStreamMeta,
|
||||
Group,
|
||||
} from "cojson";
|
||||
import React, { ChangeEvent, useEffect, useState } from "react";
|
||||
import {
|
||||
AuthProvider,
|
||||
createBinaryStreamFromBlob,
|
||||
createBrowserNode,
|
||||
} from "jazz-browser";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AuthProvider, createBrowserNode } from "jazz-browser";
|
||||
import { readBlobFromBinaryStream } from "jazz-browser";
|
||||
|
||||
export {
|
||||
@@ -183,23 +178,11 @@ export function useBinaryStream<C extends BinaryCoStream<BinaryCoStreamMeta>>(
|
||||
.catch((e) => console.error("Failed to read binary stream", e));
|
||||
}, [stream, localNode]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
blob && URL.revokeObjectURL(blob.blobURL);
|
||||
};
|
||||
}, [blob?.blobURL]);
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
export function createBinaryStreamHandler<
|
||||
C extends BinaryCoStream<BinaryCoStreamMeta>
|
||||
>(
|
||||
onCreated: (createdStream: C) => void,
|
||||
inGroup: Group,
|
||||
meta: C["meta"] = {type: "binary"}
|
||||
): (event: ChangeEvent) => void {
|
||||
return (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
createBinaryStreamFromBlob(file, inGroup, meta)
|
||||
.then(onCreated)
|
||||
.catch((e) => console.error("Failed to create binary stream", e));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "jazz-storage-indexeddb",
|
||||
"version": "0.1.12",
|
||||
"version": "0.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "^0.1.12",
|
||||
"cojson": "^0.2.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { cojsonInternals, SessionID, SyncMessage, Peer, CojsonInternalTypes } from "cojson";
|
||||
import {
|
||||
cojsonInternals,
|
||||
SessionID,
|
||||
SyncMessage,
|
||||
Peer,
|
||||
CojsonInternalTypes,
|
||||
} from "cojson";
|
||||
import {
|
||||
ReadableStream,
|
||||
WritableStream,
|
||||
@@ -209,7 +215,9 @@ export class IDBStorage {
|
||||
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
||||
sessionEntry.newTransactions.flatMap((tx) => {
|
||||
if (tx.privacy !== "trusting") return [];
|
||||
return tx.changes
|
||||
// TODO: avoid parse here?
|
||||
return cojsonInternals
|
||||
.parseJSON(tx.changes)
|
||||
.map(
|
||||
(change) =>
|
||||
change &&
|
||||
|
||||
54
yarn.lock
54
yarn.lock
@@ -1589,6 +1589,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
||||
|
||||
"@types/image-blob-reduce@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/image-blob-reduce/-/image-blob-reduce-4.1.1.tgz#3c04b47809fe5a69d652bebfc118cd74f65742bd"
|
||||
integrity sha512-Oe2EPjW+iZSsXccxZPebqHqXAUaOLir3eQVqPx0ryXeJZdCZx+gYvWBZtqYEcluP6f3bll1m06ahT26bX0+LOg==
|
||||
dependencies:
|
||||
"@types/pica" "*"
|
||||
|
||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
||||
@@ -1641,6 +1648,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||
|
||||
"@types/pica@*":
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/pica/-/pica-9.0.1.tgz#adbdfc1190bb33a9da68d1fe501c2483dae3b142"
|
||||
integrity sha512-hTsYxcy0MqIOKzeALuh3zOHyozBlndxV/bX9X52GBFq2XUQchZF6T0vcRYeT5P1ggmswi2LlIwHAH+bKWxxalg==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
@@ -4215,6 +4227,11 @@ globby@11.1.0, globby@^11.1.0:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
glur@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689"
|
||||
integrity sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==
|
||||
|
||||
"got@^ 12.6.1":
|
||||
version "12.6.1"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549"
|
||||
@@ -4449,6 +4466,13 @@ ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
|
||||
image-blob-reduce@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/image-blob-reduce/-/image-blob-reduce-4.1.0.tgz#45f1e146ceaa45079025febe307f9b1e8b6833c9"
|
||||
integrity sha512-iljleP8Fr7tS1ezrAazWi30abNPYXtBGXb9R9oTZDWObqiKq18AQJGTUb0wkBOtdCZ36/IirkuuAIIHTjBJIjA==
|
||||
dependencies:
|
||||
pica "^9.0.0"
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@@ -5918,6 +5942,14 @@ multimatch@5.0.0:
|
||||
arrify "^2.0.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
multimath@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302"
|
||||
integrity sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==
|
||||
dependencies:
|
||||
glur "^1.1.2"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
mute-stream@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
@@ -6263,7 +6295,7 @@ nx@16.6.0, "nx@>=16.5.1 < 17":
|
||||
"@nx/nx-win32-arm64-msvc" "16.6.0"
|
||||
"@nx/nx-win32-x64-msvc" "16.6.0"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
object-assign@^4.0.1, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
@@ -6615,6 +6647,16 @@ pend@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
pica@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pica/-/pica-9.0.1.tgz#9ba5a5e81fc09dca9800abef9fb8388434b18b2f"
|
||||
integrity sha512-v0U4vY6Z3ztz9b4jBIhCD3WYoecGXCQeCsYep+sXRefViL+mVVoTL+wqzdPeE+GpBFsRUtQZb6dltvAt2UkMtQ==
|
||||
dependencies:
|
||||
glur "^1.1.2"
|
||||
multimath "^2.0.0"
|
||||
object-assign "^4.1.1"
|
||||
webworkify "^1.5.0"
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
@@ -8093,11 +8135,6 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-debounce@^9.0.4:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
|
||||
integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
@@ -8292,6 +8329,11 @@ webidl-conversions@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
webworkify@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
|
||||
integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
|
||||
Reference in New Issue
Block a user