Compare commits
39 Commits
chat-rn@1.
...
jazz-nodej
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae0b8df0d | ||
|
|
77dc51d466 | ||
|
|
bd645db4cc | ||
|
|
af46c68a4a | ||
|
|
fb58cb9299 | ||
|
|
63fb80e50d | ||
|
|
133b8abcbe | ||
|
|
6209bd2285 | ||
|
|
d8d1addf2b | ||
|
|
937a34c76e | ||
|
|
15996ced64 | ||
|
|
9fb98e2114 | ||
|
|
f55f779ea1 | ||
|
|
18c98fc3f5 | ||
|
|
41b286b672 | ||
|
|
ba944c20ed | ||
|
|
0b89fadfdd | ||
|
|
1e50cebf55 | ||
|
|
ca8c5c0b02 | ||
|
|
a0aa261cab | ||
|
|
5d3d11e87c | ||
|
|
4a9ed21ea2 | ||
|
|
69499e3965 | ||
|
|
95ae69ead2 | ||
|
|
4170f13858 | ||
|
|
45e4a77afb | ||
|
|
603538e255 | ||
|
|
afb49f3666 | ||
|
|
c6de2ce8b8 | ||
|
|
cdc4229df7 | ||
|
|
fa19f7471f | ||
|
|
75f3af2cc1 | ||
|
|
5ae77ee57e | ||
|
|
87a7cf202f | ||
|
|
9a9b424ff2 | ||
|
|
dfe6146aa3 | ||
|
|
8b26728914 | ||
|
|
f5003ac8ec | ||
|
|
ee71ba99e2 |
@@ -1,5 +1,13 @@
|
|||||||
# chat-rn-expo-clerk
|
# chat-rn-expo-clerk
|
||||||
|
|
||||||
|
## 1.0.109
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-expo@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
- jazz-react-native-media-images@0.13.17
|
||||||
|
|
||||||
## 1.0.108
|
## 1.0.108
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-rn-expo-clerk",
|
"name": "chat-rn-expo-clerk",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"version": "1.0.108",
|
"version": "1.0.109",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "expo export -p ios",
|
"build": "expo export -p ios",
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# chat-rn-expo
|
# chat-rn-expo
|
||||||
|
|
||||||
|
## 1.0.96
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-expo@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 1.0.95
|
## 1.0.95
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-rn-expo",
|
"name": "chat-rn-expo",
|
||||||
"version": "1.0.95",
|
"version": "1.0.96",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "expo export -p ios",
|
"build": "expo export -p ios",
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# chat-rn
|
# chat-rn
|
||||||
|
|
||||||
|
## 1.0.104
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
- cojson-transport-ws@0.13.17
|
||||||
|
- jazz-react-native@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 1.0.103
|
## 1.0.103
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-rn",
|
"name": "chat-rn",
|
||||||
"version": "1.0.103",
|
"version": "1.0.104",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# chat-vue
|
# chat-vue
|
||||||
|
|
||||||
|
## 0.0.88
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-browser@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
- jazz-vue@0.13.17
|
||||||
|
|
||||||
## 0.0.87
|
## 0.0.87
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chat-vue",
|
"name": "chat-vue",
|
||||||
"version": "0.0.87",
|
"version": "0.0.88",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# jazz-example-chat
|
# jazz-example-chat
|
||||||
|
|
||||||
|
## 0.0.186
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-inspector@0.13.17
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.185
|
## 0.0.185
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-chat",
|
"name": "jazz-example-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.185",
|
"version": "0.0.186",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# minimal-auth-clerk
|
# minimal-auth-clerk
|
||||||
|
|
||||||
|
## 0.0.85
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-react-auth-clerk@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.84
|
## 0.0.84
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "clerk",
|
"name": "clerk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.84",
|
"version": "0.0.85",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# file-share-svelte
|
# file-share-svelte
|
||||||
|
|
||||||
|
## 0.0.68
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-svelte@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.67
|
## 0.0.67
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "file-share-svelte",
|
"name": "file-share-svelte",
|
||||||
"version": "0.0.67",
|
"version": "0.0.68",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# jazz-tailwind-demo-auth-starter
|
# jazz-tailwind-demo-auth-starter
|
||||||
|
|
||||||
|
## 0.0.25
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-inspector@0.13.17
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.24
|
## 0.0.24
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "filestream",
|
"name": "filestream",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.24",
|
"version": "0.0.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# form
|
# form
|
||||||
|
|
||||||
|
## 0.1.26
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.1.25
|
## 0.1.25
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "form",
|
"name": "form",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.25",
|
"version": "0.1.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# image-upload
|
# image-upload
|
||||||
|
|
||||||
|
## 0.0.82
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.81
|
## 0.0.81
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "image-upload",
|
"name": "image-upload",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.81",
|
"version": "0.0.82",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# jazz-example-inspector
|
# jazz-example-inspector
|
||||||
|
|
||||||
|
## 0.0.136
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
- cojson-transport-ws@0.13.17
|
||||||
|
- jazz-inspector@0.13.17
|
||||||
|
|
||||||
## 0.0.135
|
## 0.0.135
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-inspector-app",
|
"name": "jazz-inspector-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.135",
|
"version": "0.0.136",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"dev:worker": "tsx --watch --env-file=.env ./src/worker.ts",
|
"dev:worker": "tsx --watch --env-file=.env ./src/worker.ts",
|
||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"generate-env": "tsx generate-env.ts"
|
"generate-env": "tsx generate-env.ts",
|
||||||
|
"format-and-lint": "biome check .",
|
||||||
|
"format-and-lint:fix": "biome check . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
|
|||||||
1
examples/jazz-paper-scissors/src/apiKey.ts
Normal file
1
examples/jazz-paper-scissors/src/apiKey.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const apiKey = "jazz-paper-scissors@garden.co";
|
||||||
@@ -3,6 +3,7 @@ import { StrictMode } from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { apiKey } from "@/apiKey.ts";
|
||||||
import { JazzProvider } from "jazz-react";
|
import { JazzProvider } from "jazz-react";
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ if (rootElement && !rootElement.innerHTML) {
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<JazzProvider
|
<JazzProvider
|
||||||
sync={{
|
sync={{
|
||||||
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
|
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JazzInspector />
|
<JazzInspector />
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
import { Route as rootRoute } from "./routes/__root";
|
import { Route as rootRoute } from "./routes/__root";
|
||||||
import { Route as AuthenticatedImport } from "./routes/_authenticated";
|
import { Route as AuthenticatedImport } from "./routes/_authenticated";
|
||||||
import { Route as IndexImport } from "./routes/index";
|
|
||||||
import { Route as AuthenticatedWaitingRoomWaitingRoomIdImport } from "./routes/_authenticated/waiting-room.$waitingRoomId";
|
|
||||||
import { Route as AuthenticatedGameGameIdImport } from "./routes/_authenticated/game.$gameId";
|
import { Route as AuthenticatedGameGameIdImport } from "./routes/_authenticated/game.$gameId";
|
||||||
|
import { Route as AuthenticatedWaitingRoomWaitingRoomIdImport } from "./routes/_authenticated/waiting-room.$waitingRoomId";
|
||||||
|
import { Route as IndexImport } from "./routes/index";
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# multi-cursors
|
# multi-cursors
|
||||||
|
|
||||||
|
## 0.0.78
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.77
|
## 0.0.77
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-cursors",
|
"name": "multi-cursors",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.77",
|
"version": "0.0.78",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# multiauth
|
# multiauth
|
||||||
|
|
||||||
|
## 0.0.26
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-react-auth-clerk@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.25
|
## 0.0.25
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "multiauth",
|
"name": "multiauth",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.25",
|
"version": "0.0.26",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# jazz-example-musicplayer
|
# jazz-example-musicplayer
|
||||||
|
|
||||||
|
## 0.0.107
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-inspector@0.13.17
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.106
|
## 0.0.106
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-music-player",
|
"name": "jazz-example-music-player",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.106",
|
"version": "0.0.107",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# organization
|
# organization
|
||||||
|
|
||||||
|
## 0.0.78
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.77
|
## 0.0.77
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "organization",
|
"name": "organization",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.77",
|
"version": "0.0.78",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# passkey-svelte
|
# passkey-svelte
|
||||||
|
|
||||||
|
## 0.0.72
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-svelte@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.71
|
## 0.0.71
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "passkey-svelte",
|
"name": "passkey-svelte",
|
||||||
"version": "0.0.71",
|
"version": "0.0.72",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# minimal-auth-passkey
|
# minimal-auth-passkey
|
||||||
|
|
||||||
|
## 0.0.83
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.82
|
## 0.0.82
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "passkey",
|
"name": "passkey",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.82",
|
"version": "0.0.83",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# passphrase
|
# passphrase
|
||||||
|
|
||||||
|
## 0.0.80
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.79
|
## 0.0.79
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "passphrase",
|
"name": "passphrase",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.79",
|
"version": "0.0.80",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# jazz-password-manager
|
# jazz-password-manager
|
||||||
|
|
||||||
|
## 0.0.104
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.103
|
## 0.0.103
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-password-manager",
|
"name": "jazz-password-manager",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.103",
|
"version": "0.0.104",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# jazz-example-pets
|
# jazz-example-pets
|
||||||
|
|
||||||
|
## 0.0.202
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.201
|
## 0.0.201
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-pets",
|
"name": "jazz-example-pets",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.201",
|
"version": "0.0.202",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# reactions
|
# reactions
|
||||||
|
|
||||||
|
## 0.0.82
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.81
|
## 0.0.81
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "reactions",
|
"name": "reactions",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.81",
|
"version": "0.0.82",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# richtext
|
# richtext
|
||||||
|
|
||||||
|
## 0.0.72
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [133b8ab]
|
||||||
|
- jazz-richtext-prosemirror@0.1.6
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.71
|
## 0.0.71
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
A demonstration of collaborative rich text editing with Jazz, React, and ProseMirror.
|
A demonstration of collaborative rich text editing with Jazz, React, and ProseMirror.
|
||||||
|
|
||||||
|
Live version: [https://richtext-demo.jazz.tools](https://richtext-demo.jazz.tools)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This example shows how to implement collaborative rich text editing using:
|
This example shows how to implement collaborative rich text editing using:
|
||||||
@@ -16,23 +18,52 @@ The example features:
|
|||||||
- Side-by-side plaintext and rich text editors
|
- Side-by-side plaintext and rich text editors
|
||||||
- Real-time collaboration across devices
|
- Real-time collaboration across devices
|
||||||
- Persistent document storage
|
- Persistent document storage
|
||||||
|
## Getting started
|
||||||
|
|
||||||
## Running locally
|
You can either
|
||||||
|
1. Clone the jazz repository, and run the app within the monorepo.
|
||||||
|
2. Or create a new Jazz project using this example as a template.
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
|
### Using the example as a template
|
||||||
|
|
||||||
|
Create a new Jazz project, and use this example as a template.
|
||||||
```bash
|
```bash
|
||||||
npm i
|
npx create-jazz-app@latest richtext-app --example richtext
|
||||||
# or
|
|
||||||
yarn
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run the development server:
|
Go to the new project directory.
|
||||||
|
```bash
|
||||||
|
cd richtext-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the dev server.
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
# or
|
```
|
||||||
yarn dev
|
|
||||||
|
### Using the monorepo
|
||||||
|
|
||||||
|
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
|
||||||
|
|
||||||
|
Clone the jazz repository.
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/garden-co/jazz.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install and build dependencies.
|
||||||
|
```bash
|
||||||
|
pnpm i && npx turbo build
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the example directory.
|
||||||
|
```bash
|
||||||
|
cd jazz/examples/richtext/
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server.
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "richtext",
|
"name": "richtext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.71",
|
"version": "0.0.72",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"prosemirror-example-setup": "^1.2.3",
|
"prosemirror-example-setup": "^1.2.3",
|
||||||
"prosemirror-model": "^1.25.0",
|
"prosemirror-model": "^1.25.0",
|
||||||
"prosemirror-schema-basic": "^1.2.4",
|
"prosemirror-schema-basic": "^1.2.4",
|
||||||
|
"prosemirror-schema-list": "^1.5.1",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-view": "^1.39.1",
|
"prosemirror-view": "^1.39.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useAccount } from "jazz-react";
|
import { useAccount } from "jazz-react";
|
||||||
import { createJazzPlugin } from "jazz-richtext-prosemirror";
|
import { createJazzPlugin } from "jazz-richtext-prosemirror";
|
||||||
import { exampleSetup } from "prosemirror-example-setup";
|
import { exampleSetup } from "prosemirror-example-setup";
|
||||||
import { schema } from "prosemirror-schema-basic";
|
import { Schema } from "prosemirror-model";
|
||||||
|
import { schema as basicSchema } from "prosemirror-schema-basic";
|
||||||
|
import { addListNodes } from "prosemirror-schema-list";
|
||||||
import { EditorState } from "prosemirror-state";
|
import { EditorState } from "prosemirror-state";
|
||||||
import { EditorView } from "prosemirror-view";
|
import { EditorView } from "prosemirror-view";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
@@ -14,6 +16,11 @@ export function Editor() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!me || !editorRef.current || !me.profile.bio) return;
|
if (!me || !editorRef.current || !me.profile.bio) return;
|
||||||
|
|
||||||
|
const schema = new Schema({
|
||||||
|
nodes: addListNodes(basicSchema.spec.nodes, "paragraph block*", "block"),
|
||||||
|
marks: basicSchema.spec.marks,
|
||||||
|
});
|
||||||
|
|
||||||
const setupPlugins = exampleSetup({ schema });
|
const setupPlugins = exampleSetup({ schema });
|
||||||
const jazzPlugin = createJazzPlugin(me.profile.bio);
|
const jazzPlugin = createJazzPlugin(me.profile.bio);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.ProseMirror ul {
|
||||||
|
@apply list-disc;
|
||||||
|
}
|
||||||
|
.ProseMirror ol {
|
||||||
|
@apply list-decimal;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# todo-vue
|
# todo-vue
|
||||||
|
|
||||||
|
## 0.0.86
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-browser@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
- jazz-vue@0.13.17
|
||||||
|
|
||||||
## 0.0.85
|
## 0.0.85
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "todo-vue",
|
"name": "todo-vue",
|
||||||
"version": "0.0.85",
|
"version": "0.0.86",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# jazz-example-todo
|
# jazz-example-todo
|
||||||
|
|
||||||
|
## 0.0.201
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.200
|
## 0.0.200
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jazz-example-todo",
|
"name": "jazz-example-todo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.200",
|
"version": "0.0.201",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# version-history
|
# version-history
|
||||||
|
|
||||||
|
## 0.0.80
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- jazz-inspector@0.13.17
|
||||||
|
- jazz-react@0.13.17
|
||||||
|
- jazz-tools@0.13.17
|
||||||
|
|
||||||
## 0.0.79
|
## 0.0.79
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "version-history",
|
"name": "version-history",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.79",
|
"version": "0.0.80",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
|
BoldIcon,
|
||||||
BookTextIcon,
|
BookTextIcon,
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
BracesIcon,
|
BracesIcon,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
HashIcon,
|
HashIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
ItalicIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LockKeyholeIcon,
|
LockKeyholeIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
@@ -87,6 +89,10 @@ const icons = {
|
|||||||
colist: Brackets,
|
colist: Brackets,
|
||||||
user: UserIcon,
|
user: UserIcon,
|
||||||
group: UsersIcon,
|
group: UsersIcon,
|
||||||
|
|
||||||
|
// text editor icons
|
||||||
|
bold: BoldIcon,
|
||||||
|
italic: ItalicIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
// copied from tailwind line height https://tailwindcss.com/docs/font-size
|
// copied from tailwind line height https://tailwindcss.com/docs/font-size
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { packages } from "@/content/packages";
|
import { packages } from "@/content/packages";
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
|
|||||||
import { SvelteLogo } from "@/components/icons/SvelteLogo";
|
import { SvelteLogo } from "@/components/icons/SvelteLogo";
|
||||||
import { VueLogo } from "@/components/icons/VueLogo";
|
import { VueLogo } from "@/components/icons/VueLogo";
|
||||||
import { Example, features, tech } from "@/content/example";
|
import { Example, features, tech } from "@/content/example";
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
|
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
|
||||||
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
||||||
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
||||||
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
|
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
const title = "Examples";
|
const title = "Examples";
|
||||||
@@ -198,6 +198,12 @@ const MusicIllustration = () => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const JazzPaperScissorsIllustration = () => (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-8 text-4xl">
|
||||||
|
✊ ✋ ✌️
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const ImageUploadIllustration = () => (
|
const ImageUploadIllustration = () => (
|
||||||
<div className="flex flex-col items-center justify-center h-full p-8">
|
<div className="flex flex-col items-center justify-center h-full p-8">
|
||||||
<div className="p-3 w-[12rem] h-[8rem] border border-dashed border-blue dark:border-blue-500 rounded-lg flex gap-2 flex-col items-center justify-center">
|
<div className="p-3 w-[12rem] h-[8rem] border border-dashed border-blue dark:border-blue-500 rounded-lg flex gap-2 flex-col items-center justify-center">
|
||||||
@@ -255,21 +261,21 @@ const ReactionsIllustration = () => (
|
|||||||
|
|
||||||
const MultiCursorIllustration = () => (
|
const MultiCursorIllustration = () => (
|
||||||
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent p-4">
|
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent p-4">
|
||||||
<div className=" bg-white md:aspect-[3/2] flex flex-col rounded-md shadow-xl shadow-stone-400/20 dark:shadow-none">
|
<div className=" bg-white min-w-64 md:aspect-[3/2] flex flex-col rounded-md shadow-xl shadow-stone-400/20 dark:shadow-none">
|
||||||
<div className="w-full py-2 flex items-center gap-1.5 px-2 border-b dark:border-b-stone-200">
|
<div className="w-full py-2 flex items-center gap-1.5 px-2 border-b dark:border-b-stone-200">
|
||||||
<span className="rounded-full size-2 bg-stone-200"></span>
|
<span className="rounded-full size-2 bg-stone-200"></span>
|
||||||
<span className="rounded-full size-2 bg-stone-200"></span>
|
<span className="rounded-full size-2 bg-stone-200"></span>
|
||||||
<span className="rounded-full size-2 bg-stone-200"></span>
|
<span className="rounded-full size-2 bg-stone-200"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full mx-auto flex flex-col justify-center p-12 sm:p-16">
|
<div className="h-full mx-auto flex flex-col justify-center p-12">
|
||||||
<div className="inline-block relative px-1 ring-1 ring-blue-400">
|
<div className="inline-block relative px-1 ring-1 ring-blue-400">
|
||||||
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -top-1"></div>
|
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -top-1"></div>
|
||||||
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -top-1"></div>
|
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -top-1"></div>
|
||||||
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -bottom-1"></div>
|
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -bottom-1"></div>
|
||||||
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -bottom-1"></div>
|
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -bottom-1"></div>
|
||||||
|
|
||||||
<span className="text-lg font-semibold md:text-2xl md:font-bold text-stone-800 ">
|
<span className="text-lg font-semibold md:text-2xl md:font-bold text-stone-800">
|
||||||
Hello, world!
|
Hello, world!
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute -top-10 right-4 text-rose-600 flex items-end gap-1">
|
<div className="absolute -top-10 right-4 text-rose-600 flex items-end gap-1">
|
||||||
@@ -285,6 +291,21 @@ const MultiCursorIllustration = () => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const CoTextIllustration = () => (
|
||||||
|
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent p-4">
|
||||||
|
<div className=" bg-white md:aspect-[3/2] min-w-64 flex flex-col rounded-md shadow-xl shadow-stone-400/20 dark:shadow-none">
|
||||||
|
<div className="flex gap-2 p-3 border-b">
|
||||||
|
<Icon name="bold" size="xs" />
|
||||||
|
<Icon name="italic" size="xs" />
|
||||||
|
<Icon name="code" size="xs" />
|
||||||
|
</div>
|
||||||
|
<div className="py-2 px-3 text-xl text-stone-800">
|
||||||
|
<em>Hello</em>, <strong>world!</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const PetIllustration = () => (
|
const PetIllustration = () => (
|
||||||
<div className="h-full p-4 bg-[url('/dog.jpg')] bg-cover bg-center p-4 flex items-end">
|
<div className="h-full p-4 bg-[url('/dog.jpg')] bg-cover bg-center p-4 flex items-end">
|
||||||
<div className="inline-flex justify-center gap-1 mx-auto">
|
<div className="inline-flex justify-center gap-1 mx-auto">
|
||||||
@@ -434,9 +455,19 @@ const reactExamples: Example[] = [
|
|||||||
"Track user presence on a canvas with multiple cursors and out of bounds indicators.",
|
"Track user presence on a canvas with multiple cursors and out of bounds indicators.",
|
||||||
tech: [tech.react],
|
tech: [tech.react],
|
||||||
features: [features.coFeed],
|
features: [features.coFeed],
|
||||||
demoUrl: "https://jazz-multi-cursors.vercel.app",
|
demoUrl: "https://multi-cursors-demo.jazz.tools",
|
||||||
illustration: <MultiCursorIllustration />,
|
illustration: <MultiCursorIllustration />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Collaborative rich text",
|
||||||
|
slug: "richtext",
|
||||||
|
description:
|
||||||
|
"Handle multiple users editing the same text, integrated with a ProseMirror editor for rich text.",
|
||||||
|
tech: [tech.react],
|
||||||
|
features: [features.coRichText, features.coPlainText],
|
||||||
|
demoUrl: "https://richtext-demo.jazz.tools",
|
||||||
|
illustration: <CoTextIllustration />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Rate my pet",
|
name: "Rate my pet",
|
||||||
slug: "pets",
|
slug: "pets",
|
||||||
@@ -477,6 +508,16 @@ const reactExamples: Example[] = [
|
|||||||
demoUrl: "https://music-demo.jazz.tools",
|
demoUrl: "https://music-demo.jazz.tools",
|
||||||
illustration: <MusicIllustration />,
|
illustration: <MusicIllustration />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Jazz paper scissors",
|
||||||
|
slug: "jazz-paper-scissors",
|
||||||
|
description:
|
||||||
|
"A game that shows how to communicate with other accounts through the experimental Inbox API.",
|
||||||
|
tech: [tech.react],
|
||||||
|
features: [features.serverWorker, features.inbox],
|
||||||
|
illustration: <JazzPaperScissorsIllustration />,
|
||||||
|
demoUrl: "https://jazz-paper-scissors.vercel.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Clerk",
|
name: "Clerk",
|
||||||
slug: "clerk",
|
slug: "clerk",
|
||||||
@@ -511,6 +552,7 @@ const reactExamples: Example[] = [
|
|||||||
tech: [tech.react],
|
tech: [tech.react],
|
||||||
features: [features.inviteLink],
|
features: [features.inviteLink],
|
||||||
illustration: <OrganizationIllustration />,
|
illustration: <OrganizationIllustration />,
|
||||||
|
demoUrl: "https://jazz-organization.vercel.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Version history",
|
name: "Version history",
|
||||||
@@ -519,6 +561,7 @@ const reactExamples: Example[] = [
|
|||||||
"Track and restore previous versions of your data, and see who made the changes.",
|
"Track and restore previous versions of your data, and see who made the changes.",
|
||||||
tech: [tech.react],
|
tech: [tech.react],
|
||||||
illustration: <VersionHistoryIllustration />,
|
illustration: <VersionHistoryIllustration />,
|
||||||
|
demoUrl: "https://jazz-version-history.vercel.app"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -566,7 +609,6 @@ const vueExamples: Example[] = [
|
|||||||
description: "A todo list where you can collaborate with invited guests.",
|
description: "A todo list where you can collaborate with invited guests.",
|
||||||
tech: [tech.vue],
|
tech: [tech.vue],
|
||||||
features: [features.inviteLink],
|
features: [features.inviteLink],
|
||||||
demoUrl: "https://todo-demo.jazz.tools",
|
|
||||||
illustration: (
|
illustration: (
|
||||||
<div className="h-full w-full bg-cover bg-[url('/todo.jpg')] bg-left-bottom"></div>
|
<div className="h-full w-full bg-cover bg-[url('/todo.jpg')] bg-left-bottom"></div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import LatencyChart from "@/components/LatencyChart";
|
import LatencyChart from "@/components/LatencyChart";
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
|
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { fontClasses } from "@garden-co/design-system/src/fonts";
|
|
||||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
import { JazzFooter } from "@/components/footer";
|
import { JazzFooter } from "@/components/footer";
|
||||||
import { marketingCopy } from "@/content/marketingCopy";
|
import { marketingCopy } from "@/content/marketingCopy";
|
||||||
|
import { fontClasses } from "@garden-co/design-system/src/fonts";
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
const metaTags = {
|
const metaTags = {
|
||||||
title: `Jazz - ${marketingCopy.headline}`,
|
title: `Jazz - ${marketingCopy.headline}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { track } from "@vercel/analytics";
|
|
||||||
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
||||||
|
import { track } from "@vercel/analytics";
|
||||||
|
|
||||||
export function FakeGetStartedButton({ tier }: { tier: "starter" | "indie" }) {
|
export function FakeGetStartedButton({ tier }: { tier: "starter" | "indie" }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clsx } from "clsx";
|
|
||||||
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import {
|
import {
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
LucideBuilding2,
|
LucideBuilding2,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { JazzNav } from "@/components/nav";
|
import { JazzNav } from "@/components/nav";
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
|
import { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
export function SideNavLayout({
|
export function SideNavLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { TableOfContents } from "@/components/docs/TableOfContents";
|
import { TableOfContents } from "@/components/docs/TableOfContents";
|
||||||
import { JazzMobileNav } from "@/components/nav";
|
import { JazzMobileNav } from "@/components/nav";
|
||||||
import { TocEntry } from "@stefanprobst/rehype-extract-toc";
|
|
||||||
import type { IconName } from "@garden-co/design-system/src/components/atoms/Icon";
|
import type { IconName } from "@garden-co/design-system/src/components/atoms/Icon";
|
||||||
import { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
|
import { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
|
||||||
|
import { TocEntry } from "@stefanprobst/rehype-extract-toc";
|
||||||
|
|
||||||
export default function DocsLayout({
|
export default function DocsLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { Example } from "@/content/example";
|
import { Example } from "@/content/example";
|
||||||
import { InterpolateInCode } from "@/mdx-components";
|
import { InterpolateInCode } from "@/mdx-components";
|
||||||
import { DialogDescription } from "@headlessui/react";
|
|
||||||
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
|
||||||
import { CodeGroup } from "@garden-co/design-system/src/components/molecules/CodeGroup";
|
import { CodeGroup } from "@garden-co/design-system/src/components/molecules/CodeGroup";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
DialogBody,
|
DialogBody,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@garden-co/design-system/src/components/organisms/Dialog";
|
} from "@garden-co/design-system/src/components/organisms/Dialog";
|
||||||
|
import { DialogDescription } from "@headlessui/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import CreateJazzApp from "./CreateJazzApp.mdx";
|
import CreateJazzApp from "./CreateJazzApp.mdx";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { clsx } from "clsx";
|
|
||||||
import { Card } from "@garden-co/design-system/src/components/atoms/Card";
|
import { Card } from "@garden-co/design-system/src/components/atoms/Card";
|
||||||
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
|
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
|
||||||
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
|
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
|
||||||
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
||||||
|
import { clsx } from "clsx";
|
||||||
import CodeStepAction from "./CodeStepAction.mdx";
|
import CodeStepAction from "./CodeStepAction.mdx";
|
||||||
import CodeStepCloud from "./CodeStepCloud.mdx";
|
import CodeStepCloud from "./CodeStepCloud.mdx";
|
||||||
import CodeStepRender from "./CodeStepRender.mdx";
|
import CodeStepRender from "./CodeStepRender.mdx";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -134,9 +134,20 @@ export async function onAnonymousAccountDiscarded(
|
|||||||
|
|
||||||
To see how this works, try uploading a song in the [music player demo](https://music-demo.jazz.tools/) and then log in with an existing account.
|
To see how this works, try uploading a song in the [music player demo](https://music-demo.jazz.tools/) and then log in with an existing account.
|
||||||
|
|
||||||
|
## Provider Configuration for Authentication
|
||||||
|
|
||||||
|
You can configure how authentication states work in your app with the [JazzProvider](/docs/project-setup/providers/). The provider offers several options that impact authentication behavior:
|
||||||
|
|
||||||
|
- `guestMode`: Enable/disable Guest Mode
|
||||||
|
- `onAnonymousAccountDiscarded`: Handle data migration when switching accounts
|
||||||
|
- `sync.when`: Control when data synchronization happens
|
||||||
|
- `defaultProfileName`: Set default name for new user profiles
|
||||||
|
|
||||||
|
For detailed information on all provider options, see [Provider Configuration options](/docs/project-setup/providers/#additional-options).
|
||||||
|
|
||||||
## Controlling sync for different authentication states
|
## Controlling sync for different authentication states
|
||||||
|
|
||||||
You can control network sync with [Providers](/docs/project-setup/providers) based on authentication state:
|
You can control network sync with [Providers](/docs/project-setup/providers/) based on authentication state:
|
||||||
|
|
||||||
- `when: "always"`: Sync is enabled for both Anonymous Authentication and Authenticated Account
|
- `when: "always"`: Sync is enabled for both Anonymous Authentication and Authenticated Account
|
||||||
- `when: "signedUp"`: Sync is enabled when the user is authenticated
|
- `when: "signedUp"`: Sync is enabled when the user is authenticated
|
||||||
@@ -197,7 +208,7 @@ function App() {
|
|||||||
|
|
||||||
### Configuring Guest Mode Access
|
### Configuring Guest Mode Access
|
||||||
|
|
||||||
You can configure Guest Mode access with the `guestMode` prop for [Providers](/docs/project-setup/providers).
|
You can configure Guest Mode access with the `guestMode` prop for [Providers](/docs/project-setup/providers/).
|
||||||
|
|
||||||
<ContentByFramework framework="react">
|
<ContentByFramework framework="react">
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const docNavigationItems = [
|
|||||||
name: "Providers",
|
name: "Providers",
|
||||||
href: "/docs/project-setup/providers",
|
href: "/docs/project-setup/providers",
|
||||||
done: {
|
done: {
|
||||||
|
react: 100,
|
||||||
"react-native": 100,
|
"react-native": 100,
|
||||||
"react-native-expo": 100,
|
"react-native-expo": 100,
|
||||||
},
|
},
|
||||||
@@ -168,6 +169,11 @@ export const docNavigationItems = [
|
|||||||
href: "/docs/using-covalues/cofeeds",
|
href: "/docs/using-covalues/cofeeds",
|
||||||
done: 100,
|
done: 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "CoTexts",
|
||||||
|
href: "/docs/using-covalues/cotexts",
|
||||||
|
done: 100,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "FileStreams",
|
name: "FileStreams",
|
||||||
href: "/docs/using-covalues/filestreams",
|
href: "/docs/using-covalues/filestreams",
|
||||||
|
|||||||
195
homepage/homepage/content/docs/project-setup/providers/react.mdx
Normal file
195
homepage/homepage/content/docs/project-setup/providers/react.mdx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
export const metadata = { title: "Providers" };
|
||||||
|
|
||||||
|
import { CodeGroup } from "@/components/forMdx";
|
||||||
|
|
||||||
|
# Providers
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
`<JazzProvider />` is the core component that connects your React application to Jazz. It handles:
|
||||||
|
|
||||||
|
- **Data Synchronization**: Manages connections to peers and the Jazz cloud
|
||||||
|
- **Local Storage**: Persists data locally between app sessions
|
||||||
|
- **Schema Types**: Provides APIs for the [AccountSchema](/docs/schemas/accounts-and-migrations)
|
||||||
|
- **Authentication**: Connects your authentication system to Jazz
|
||||||
|
|
||||||
|
Our [Chat example app](https://jazz.tools/examples#chat) provides a complete implementation of JazzProvider with authentication and real-time data sync.
|
||||||
|
|
||||||
|
## Setting up the Provider
|
||||||
|
|
||||||
|
The `<JazzProvider />` accepts several configuration options:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
// @filename: schema.ts
|
||||||
|
import { Account, co } from "jazz-tools";
|
||||||
|
|
||||||
|
export class MyAppAccount extends Account {
|
||||||
|
name = co.string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @filename: app.tsx
|
||||||
|
import * as React from "react";
|
||||||
|
// ---cut---
|
||||||
|
// App.tsx
|
||||||
|
import { JazzProvider } from "jazz-react";
|
||||||
|
import { MyAppAccount } from "./schema";
|
||||||
|
|
||||||
|
export function MyApp({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<JazzProvider
|
||||||
|
sync={{
|
||||||
|
peer: "wss://cloud.jazz.tools/?key=your-api-key",
|
||||||
|
when: "always" // When to sync: "always", "never", or "signedUp"
|
||||||
|
}}
|
||||||
|
AccountSchema={MyAppAccount}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</JazzProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||||
|
declare module "jazz-react" {
|
||||||
|
interface Register {
|
||||||
|
Account: MyAppAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Provider Options
|
||||||
|
|
||||||
|
### Sync Options
|
||||||
|
|
||||||
|
The `sync` property configures how your application connects to the Jazz network:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
import { type SyncConfig } from "jazz-tools";
|
||||||
|
|
||||||
|
const syncConfig: SyncConfig = {
|
||||||
|
// Connection to Jazz Cloud or your own sync server
|
||||||
|
peer: "wss://cloud.jazz.tools/?key=your-api-key",
|
||||||
|
|
||||||
|
// When to sync: "always" (default), "never", or "signedUp"
|
||||||
|
when: "always",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
See [Authentication States](/docs/authentication/authentication-states#controlling-sync-for-different-authentication-states) for more details on how the `when` property affects synchronization based on authentication state.
|
||||||
|
|
||||||
|
### Account Schema
|
||||||
|
|
||||||
|
The `AccountSchema` property defines your application's account structure:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
|
||||||
|
// @filename: schema.ts
|
||||||
|
import { Account, CoMap, co } from "jazz-tools";
|
||||||
|
// schema.ts
|
||||||
|
class Preferences extends CoMap {
|
||||||
|
theme = co.string;
|
||||||
|
notifications = co.boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define your custom account schema
|
||||||
|
export class MyAppAccount extends Account {
|
||||||
|
name = co.string;
|
||||||
|
preferences = co.ref(Preferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @filename: app.tsx
|
||||||
|
import * as React from "react";
|
||||||
|
import { JazzProvider } from "jazz-react";
|
||||||
|
import { SyncConfig } from "jazz-tools";
|
||||||
|
|
||||||
|
const syncConfig: SyncConfig = {
|
||||||
|
peer: "wss://cloud.jazz.tools/?key=your-api-key",
|
||||||
|
when: "always",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// app.tsx
|
||||||
|
import { MyAppAccount } from "./schema";
|
||||||
|
|
||||||
|
export function MyApp ({ children }: { children: React.ReactNode }) {
|
||||||
|
// Use in provider
|
||||||
|
return (
|
||||||
|
<JazzProvider
|
||||||
|
sync={syncConfig}
|
||||||
|
|
||||||
|
AccountSchema={MyAppAccount}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</JazzProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register type for useAccount
|
||||||
|
declare module "jazz-react" {
|
||||||
|
interface Register {
|
||||||
|
Account: MyAppAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Additional Options
|
||||||
|
|
||||||
|
The provider accepts these additional options:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
import * as React from "react";
|
||||||
|
import { JazzProvider } from "jazz-react";
|
||||||
|
import { SyncConfig } from "jazz-tools";
|
||||||
|
|
||||||
|
const syncConfig: SyncConfig = {
|
||||||
|
peer: "wss://cloud.jazz.tools/?key=your-api-key",
|
||||||
|
when: "always",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// app.tsx
|
||||||
|
export function MyApp ({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<JazzProvider
|
||||||
|
sync={syncConfig}
|
||||||
|
|
||||||
|
// Enable guest mode for account-less access
|
||||||
|
guestMode={false}
|
||||||
|
|
||||||
|
// Set default name for new user profiles
|
||||||
|
defaultProfileName="New User"
|
||||||
|
|
||||||
|
// Handle user logout
|
||||||
|
onLogOut={() => {
|
||||||
|
console.log("User logged out");
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Handle anonymous account data when user logs in to existing account
|
||||||
|
onAnonymousAccountDiscarded={(account) => {
|
||||||
|
console.log("Anonymous account discarded", account.id);
|
||||||
|
// Migrate data here
|
||||||
|
return Promise.resolve();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</JazzProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
See [Authentication States](/docs/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
`<JazzProvider />` works with various authentication methods to enable users to access their data across multiple devices. For a complete guide to authentication, see our [Authentication Overview](/docs/authentication/overview).
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you have questions about configuring the Jazz Provider for your specific use case, [join our Discord community](https://discord.gg/utDMjHYg42) for help.
|
||||||
437
homepage/homepage/content/docs/using-covalues/cotexts.mdx
Normal file
437
homepage/homepage/content/docs/using-covalues/cotexts.mdx
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
import { CodeGroup, ContentByFramework } from "@/components/forMdx";
|
||||||
|
|
||||||
|
export const metadata = { title: "CoTexts" };
|
||||||
|
|
||||||
|
# CoTexts
|
||||||
|
|
||||||
|
Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values:
|
||||||
|
|
||||||
|
- **CoPlainText** for simple text editing without formatting
|
||||||
|
- **CoRichText** for rich text with HTML-based formatting (extends CoPlainText)
|
||||||
|
|
||||||
|
Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users.
|
||||||
|
|
||||||
|
**Note:** If you're looking for a quick way to add rich text editing to your app, check out [jazz-richtext-prosemirror](#using-rich-text-with-prosemirror).
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
const note = CoPlainText.create("Meeting notes", { owner: me });
|
||||||
|
|
||||||
|
// Update the text
|
||||||
|
note.applyDiff("Meeting notes for Tuesday");
|
||||||
|
|
||||||
|
console.log(note.toString()); // "Meeting notes for Tuesday"
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
For a full example of CoTexts in action, see [our Richtext example app](https://github.com/garden-co/jazz/tree/main/examples/richtext), which shows plain text and rich text editing.
|
||||||
|
|
||||||
|
## CoPlainText vs co.string
|
||||||
|
|
||||||
|
While `co.string` is perfect for simple text fields, `CoPlainText` is the right choice when you need:
|
||||||
|
|
||||||
|
- Multiple users editing the same text simultaneously
|
||||||
|
- Fine-grained control over text edits (inserting, deleting at specific positions)
|
||||||
|
- Character-by-character collaboration
|
||||||
|
- Efficient merging of concurrent changes
|
||||||
|
|
||||||
|
Both support real-time updates, but `CoPlainText` provides specialized tools for collaborative editing scenarios.
|
||||||
|
|
||||||
|
## Creating CoText Values
|
||||||
|
|
||||||
|
CoText values are typically used as fields in your schemas:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoMap, CoPlainText, CoRichText, co } from "jazz-tools";
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
class Profile extends CoMap {
|
||||||
|
name = co.string;
|
||||||
|
bio = co.ref(CoPlainText); // Plain text field
|
||||||
|
description = co.ref(CoRichText); // Rich text with formatting
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Create a CoText value with a simple string:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, CoRichText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// Create plaintext with default ownership (current user)
|
||||||
|
const note = CoPlainText.create("Meeting notes", { owner: me });
|
||||||
|
|
||||||
|
// Create rich text with HTML content
|
||||||
|
const document = CoRichText.create("<p>Project <strong>overview</strong></p>",
|
||||||
|
{ owner: me }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Ownership
|
||||||
|
|
||||||
|
Like other CoValues, you can specify ownership when creating CoTexts.
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, Group, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
const colleagueAccount = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// Create with shared ownership
|
||||||
|
const teamGroup = Group.create();
|
||||||
|
teamGroup.addMember(colleagueAccount, "writer");
|
||||||
|
|
||||||
|
const teamNote = CoPlainText.create("Team updates", { owner: teamGroup });
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoText values.
|
||||||
|
|
||||||
|
## Reading Text
|
||||||
|
|
||||||
|
CoText values work like JavaScript strings:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
const note = CoPlainText.create("Meeting notes", { owner: me });
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// Get the text content
|
||||||
|
console.log(note.toString()); // "Meeting notes"
|
||||||
|
|
||||||
|
// Check the text length
|
||||||
|
console.log(note.length); // 14
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
## Making Edits
|
||||||
|
|
||||||
|
Insert and delete text with intuitive methods:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
const note = CoPlainText.create("Meeting notes", { owner: me });
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// Insert text at a specific position
|
||||||
|
note.insertBefore(8, "weekly "); // "Meeting weekly notes"
|
||||||
|
|
||||||
|
// Insert after a position
|
||||||
|
note.insertAfter(21, " for Monday"); // "Meeting weekly notes for Monday"
|
||||||
|
|
||||||
|
// Delete a range of text
|
||||||
|
note.deleteRange({ from: 8, to: 15 }); // "Meeting notes for Monday"
|
||||||
|
|
||||||
|
// Apply a diff to update the entire text
|
||||||
|
note.applyDiff("Team meeting notes for Tuesday");
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Applying Diffs
|
||||||
|
|
||||||
|
Use `applyDiff` to efficiently update text with minimal changes:
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
// Original text: "Team status update"
|
||||||
|
const minutes = CoPlainText.create("Team status update", { owner: me });
|
||||||
|
|
||||||
|
// Replace the entire text with a new version
|
||||||
|
minutes.applyDiff("Weekly team status update for Project X");
|
||||||
|
|
||||||
|
// Make partial changes
|
||||||
|
let text = minutes.toString();
|
||||||
|
text = text.replace("Weekly", "Monday");
|
||||||
|
minutes.applyDiff(text); // Efficiently updates only what changed
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
Perfect for handling user input in form controls:
|
||||||
|
|
||||||
|
<ContentByFramework framework="react">
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
import React, { useState } from "react";
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
function TextEditor() {
|
||||||
|
const [note, setNote] = useState(CoPlainText.create("", { owner: me }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
value={note.toString()}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Efficiently update only what the user changed
|
||||||
|
note.applyDiff(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="vanilla">
|
||||||
|
<CodeGroup>
|
||||||
|
```ts twoslash
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
const note = CoPlainText.create("", { owner: me });
|
||||||
|
|
||||||
|
// Create and set up the textarea
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = note.toString();
|
||||||
|
|
||||||
|
// Add event listener for changes
|
||||||
|
textarea.addEventListener('input', (e: Event) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
// Efficiently update only what the user changed
|
||||||
|
note.applyDiff(target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the textarea to the document
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="vue">
|
||||||
|
<CodeGroup>
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { CoPlainText } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
|
||||||
|
const note = ref(null);
|
||||||
|
const textContent = ref("");
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
note.value = CoPlainText.create("", { owner: me });
|
||||||
|
textContent.value = note.value.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateText(e) {
|
||||||
|
if (note.value) {
|
||||||
|
note.value.applyDiff(e.target.value);
|
||||||
|
textContent.value = note.value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<textarea
|
||||||
|
:value="textContent"
|
||||||
|
@input="updateText"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="svelte">
|
||||||
|
<CodeGroup>
|
||||||
|
```svelte twoslash
|
||||||
|
<script lang="ts">
|
||||||
|
import { CoPlainText, Account } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const me = await createJazzTestAccount();
|
||||||
|
|
||||||
|
const note = CoPlainText.create("", { owner: me });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={note.toString()}
|
||||||
|
oninput={e => note.applyDiff(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
## Using Rich Text with ProseMirror
|
||||||
|
|
||||||
|
Jazz provides a dedicated plugin for integrating CoRichText with the popular ProseMirror editor. This plugin, [`jazz-richtext-prosemirror`](https://www.npmjs.com/package/jazz-richtext-prosemirror), enables bidirectional synchronization between your CoRichText instances and ProseMirror editors.
|
||||||
|
|
||||||
|
### ProseMirror Plugin Features
|
||||||
|
|
||||||
|
- **Bidirectional Sync**: Changes in the editor automatically update the CoRichText and vice versa
|
||||||
|
- **Real-time Collaboration**: Multiple users can edit the same document simultaneously
|
||||||
|
- **HTML Conversion**: Automatically converts between HTML (used by CoRichText) and ProseMirror's document model
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```bash
|
||||||
|
pnpm add jazz-richtext-prosemirror \
|
||||||
|
prosemirror-view \
|
||||||
|
prosemirror-state \
|
||||||
|
prosemirror-schema-basic
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
|
||||||
|
<ContentByFramework framework="react-native">
|
||||||
|
We don't currently have a React Native-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="react-native-expo">
|
||||||
|
We don't currently have a React Native Expo-specific example, but you need help please [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework={["react", "react-native", "react-native-expo"]}>
|
||||||
|
For use with React:
|
||||||
|
<CodeGroup>
|
||||||
|
```tsx twoslash
|
||||||
|
class JazzProfile extends Profile {
|
||||||
|
bio = co.ref(CoRichText);
|
||||||
|
}
|
||||||
|
|
||||||
|
class JazzAccount extends Account {
|
||||||
|
profile = co.ref(JazzProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "jazz-react" {
|
||||||
|
interface Register {
|
||||||
|
Account: JazzAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { useAccount, useCoState } from "jazz-react";
|
||||||
|
import { CoRichText, Account, Profile, co } from "jazz-tools";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
// ---cut---
|
||||||
|
// RichTextEditor.tsx
|
||||||
|
import { createJazzPlugin } from "jazz-richtext-prosemirror";
|
||||||
|
import { exampleSetup } from "prosemirror-example-setup";
|
||||||
|
import { schema } from "prosemirror-schema-basic";
|
||||||
|
import { EditorState } from "prosemirror-state";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
|
||||||
|
function RichTextEditor() {
|
||||||
|
const { me } = useAccount({ resolve: { profile: true } });
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const viewRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!me?.profile.bio || !editorRef.current) return;
|
||||||
|
|
||||||
|
// Create the Jazz plugin for ProseMirror
|
||||||
|
// Providing a CoRichText instance to the plugin to automatically sync changes
|
||||||
|
const jazzPlugin = createJazzPlugin(me.profile.bio); // [!code ++]
|
||||||
|
|
||||||
|
// Set up ProseMirror with the Jazz plugin
|
||||||
|
if (!viewRef.current) {
|
||||||
|
viewRef.current = new EditorView(editorRef.current, {
|
||||||
|
state: EditorState.create({
|
||||||
|
schema,
|
||||||
|
plugins: [
|
||||||
|
...exampleSetup({ schema }),
|
||||||
|
jazzPlugin, // [!code ++]
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (viewRef.current) {
|
||||||
|
viewRef.current.destroy();
|
||||||
|
viewRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [me?.profile.bio?.id]);
|
||||||
|
|
||||||
|
if (!me) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded">
|
||||||
|
<div ref={editorRef} className="p-2" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="svelte">
|
||||||
|
We don't currently have a Svelte-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework="vue">
|
||||||
|
We don't currently have a Vue-specific example, but you need help you can [request one](https://github.com/garden-co/jazz/issues/new), or ask on [Discord](https://discord.gg/utDMjHYg42).
|
||||||
|
</ContentByFramework>
|
||||||
|
|
||||||
|
<ContentByFramework framework={["vanilla", "svelte", "vue", "react-native", "react-native-expo"]}>
|
||||||
|
For use without a framework:
|
||||||
|
<CodeGroup>
|
||||||
|
```js twoslash
|
||||||
|
import { CoRichText } from "jazz-tools";
|
||||||
|
import { createJazzPlugin } from "jazz-richtext-prosemirror";
|
||||||
|
import { exampleSetup } from "prosemirror-example-setup";
|
||||||
|
import { schema } from "prosemirror-schema-basic";
|
||||||
|
import { EditorState } from "prosemirror-state";
|
||||||
|
import { EditorView } from "prosemirror-view";
|
||||||
|
|
||||||
|
function setupRichTextEditor(coRichText, container) {
|
||||||
|
// Create the Jazz plugin for ProseMirror
|
||||||
|
// Providing a CoRichText instance to the plugin to automatically sync changes
|
||||||
|
const jazzPlugin = createJazzPlugin(coRichText); // [!code ++]
|
||||||
|
|
||||||
|
// Set up ProseMirror with Jazz plugin
|
||||||
|
const view = new EditorView(container, {
|
||||||
|
state: EditorState.create({
|
||||||
|
schema,
|
||||||
|
plugins: [
|
||||||
|
...exampleSetup({ schema }),
|
||||||
|
jazzPlugin, // [!code ++]
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
view.destroy();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const document = CoRichText.create("<p>Initial content</p>", { owner: me });
|
||||||
|
const editorContainer = document.getElementById("editor");
|
||||||
|
const cleanup = setupRichTextEditor(document, editorContainer);
|
||||||
|
|
||||||
|
// Later when done with the editor
|
||||||
|
cleanup();
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
</ContentByFramework>
|
||||||
@@ -18,7 +18,8 @@ FileStreams provide automatic chunking when using the `createFromBlob` method, t
|
|||||||
In your schema, reference FileStreams like any other CoValue:
|
In your schema, reference FileStreams like any other CoValue:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
// schema.ts
|
||||||
import { CoMap, FileStream, co } from "jazz-tools";
|
import { CoMap, FileStream, co } from "jazz-tools";
|
||||||
|
|
||||||
class Document extends CoMap {
|
class Document extends CoMap {
|
||||||
@@ -37,25 +38,33 @@ There are two main ways to create FileStreams: creating empty ones for manual da
|
|||||||
For files from input elements or drag-and-drop interfaces, use `createFromBlob`:
|
For files from input elements or drag-and-drop interfaces, use `createFromBlob`:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
// @errors: 18047
|
||||||
|
import { FileStream, Group } from "jazz-tools";
|
||||||
|
const myGroup = Group.create();
|
||||||
|
const progressBar: HTMLElement = document.querySelector('.progress-bar')!;
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
// From a file input
|
// From a file input
|
||||||
const fileInput = document.querySelector('input[type="file"]');
|
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
|
|
||||||
fileInput.addEventListener('change', async () => {
|
fileInput.addEventListener('change', async () => {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files?.[0];
|
||||||
if (file) {
|
if (!file) return;
|
||||||
// Create FileStream from user-selected file
|
|
||||||
const fileStream = await FileStream.createFromBlob(file);
|
// Create FileStream from user-selected file
|
||||||
|
const fileStream = await FileStream.createFromBlob(file, { owner: myGroup });
|
||||||
// Or with progress tracking for better UX
|
|
||||||
const fileWithProgress = await FileStream.createFromBlob(file, {
|
// Or with progress tracking for better UX
|
||||||
onProgress: (progress) => {
|
const fileWithProgress = await FileStream.createFromBlob(file, {
|
||||||
// progress is a value between 0 and 1
|
onProgress: (progress) => {
|
||||||
const percent = Math.round(progress * 100);
|
// progress is a value between 0 and 1
|
||||||
console.log(`Upload progress: ${percent}%`);
|
const percent = Math.round(progress * 100);
|
||||||
progressBar.style.width = `${percent}%`;
|
console.log(`Upload progress: ${percent}%`);
|
||||||
}
|
progressBar.style.width = `${percent}%`;
|
||||||
});
|
},
|
||||||
}
|
owner: myGroup
|
||||||
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
@@ -65,11 +74,12 @@ fileInput.addEventListener('change', async () => {
|
|||||||
Create an empty FileStream when you want to manually [add binary data in chunks](/docs/using-covalues/filestreams#writing-to-filestreams):
|
Create an empty FileStream when you want to manually [add binary data in chunks](/docs/using-covalues/filestreams#writing-to-filestreams):
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
import { FileStream } from "jazz-tools";
|
import { Group, FileStream } from "jazz-tools";
|
||||||
|
const myGroup = Group.create();
|
||||||
|
// ---cut---
|
||||||
// Create a new empty FileStream
|
// Create a new empty FileStream
|
||||||
const fileStream = FileStream.create();
|
const fileStream = FileStream.create({ owner: myGroup } );
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
@@ -105,7 +115,10 @@ const teamFileStream = FileStream.create({ owner: teamGroup });
|
|||||||
To access the raw binary data and metadata:
|
To access the raw binary data and metadata:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
// Get all chunks and metadata
|
// Get all chunks and metadata
|
||||||
const fileData = fileStream.getChunks();
|
const fileData = fileStream.getChunks();
|
||||||
|
|
||||||
@@ -127,7 +140,10 @@ if (fileData) {
|
|||||||
By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option:
|
By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
// Get data even if the stream isn't complete
|
// Get data even if the stream isn't complete
|
||||||
const partialData = fileStream.getChunks({ allowUnfinished: true });
|
const partialData = fileStream.getChunks({ allowUnfinished: true });
|
||||||
```
|
```
|
||||||
@@ -138,10 +154,16 @@ const partialData = fileStream.getChunks({ allowUnfinished: true });
|
|||||||
For easier integration with web APIs, convert to a `Blob`:
|
For easier integration with web APIs, convert to a `Blob`:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
// Convert to a Blob
|
// Convert to a Blob
|
||||||
const blob = fileStream.toBlob();
|
const blob = fileStream.toBlob();
|
||||||
|
|
||||||
|
// Get the filename from the metadata
|
||||||
|
const filename = fileStream.getChunks()?.fileName;
|
||||||
|
|
||||||
if (blob) {
|
if (blob) {
|
||||||
// Use with URL.createObjectURL
|
// Use with URL.createObjectURL
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -149,7 +171,7 @@ if (blob) {
|
|||||||
// Create a download link
|
// Create a download link
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = fileData?.fileName || 'document.pdf';
|
link.download = filename || 'document.pdf';
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
// Clean up when done
|
// Clean up when done
|
||||||
@@ -163,7 +185,10 @@ if (blob) {
|
|||||||
You can directly load a `FileStream` as a `Blob` when you only have its ID:
|
You can directly load a `FileStream` as a `Blob` when you only have its ID:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream, type ID } from "jazz-tools";
|
||||||
|
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||||
|
// ---cut---
|
||||||
// Load directly as a Blob when you have an ID
|
// Load directly as a Blob when you have an ID
|
||||||
const blob = await FileStream.loadAsBlob(fileStreamId);
|
const blob = await FileStream.loadAsBlob(fileStreamId);
|
||||||
|
|
||||||
@@ -180,7 +205,10 @@ const partialBlob = await FileStream.loadAsBlob(fileStreamId, {
|
|||||||
Check if a `FileStream` is fully synced:
|
Check if a `FileStream` is fully synced:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
if (fileStream.isBinaryStreamEnded()) {
|
if (fileStream.isBinaryStreamEnded()) {
|
||||||
console.log('File is completely synced');
|
console.log('File is completely synced');
|
||||||
} else {
|
} else {
|
||||||
@@ -206,9 +234,12 @@ When creating a `FileStream` manually (not using `createFromBlob`), you need to
|
|||||||
Begin by providing metadata about the file:
|
Begin by providing metadata about the file:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream, Group } from "jazz-tools";
|
||||||
|
const myGroup = Group.create();
|
||||||
|
// ---cut---
|
||||||
// Create an empty FileStream
|
// Create an empty FileStream
|
||||||
const fileStream = FileStream.create();
|
const fileStream = FileStream.create({ owner: myGroup });
|
||||||
|
|
||||||
// Initialize with metadata
|
// Initialize with metadata
|
||||||
fileStream.start({
|
fileStream.start({
|
||||||
@@ -224,9 +255,15 @@ fileStream.start({
|
|||||||
Add binary data in chunks - this helps with large files and progress tracking:
|
Add binary data in chunks - this helps with large files and progress tracking:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
// Create a sample Uint8Array (in real apps, this would be file data)
|
import { FileStream } from "jazz-tools";
|
||||||
const data = new Uint8Array([...]);
|
const fileStream = FileStream.create();
|
||||||
|
const file = [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64]; // "Hello World" in ASCII
|
||||||
|
const bytes = new Uint8Array(file);
|
||||||
|
const arrayBuffer = bytes.buffer;
|
||||||
|
|
||||||
|
// ---cut---
|
||||||
|
const data = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
// For large files, break into chunks (e.g., 100KB each)
|
// For large files, break into chunks (e.g., 100KB each)
|
||||||
const chunkSize = 1024 * 100;
|
const chunkSize = 1024 * 100;
|
||||||
@@ -249,7 +286,10 @@ for (let i = 0; i < data.length; i += chunkSize) {
|
|||||||
Once all chunks are pushed, mark the `FileStream` as complete:
|
Once all chunks are pushed, mark the `FileStream` as complete:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
// Finalize the upload
|
// Finalize the upload
|
||||||
fileStream.end();
|
fileStream.end();
|
||||||
|
|
||||||
@@ -266,9 +306,12 @@ Like other CoValues, you can subscribe to `FileStream`s to get notified of chang
|
|||||||
Load a `FileStream` when you have its ID:
|
Load a `FileStream` when you have its ID:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream, type ID } from "jazz-tools";
|
||||||
|
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||||
|
// ---cut---
|
||||||
// Load a FileStream by ID
|
// Load a FileStream by ID
|
||||||
const fileStream = await FileStream.load(fileStreamId, []);
|
const fileStream = await FileStream.load(fileStreamId);
|
||||||
|
|
||||||
if (fileStream) {
|
if (fileStream) {
|
||||||
console.log('FileStream loaded successfully');
|
console.log('FileStream loaded successfully');
|
||||||
@@ -287,16 +330,20 @@ if (fileStream) {
|
|||||||
Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete:
|
Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream, type ID } from "jazz-tools";
|
||||||
|
import { createJazzTestAccount } from 'jazz-tools/testing';
|
||||||
|
const fileStreamId = "co_z123" as ID<FileStream>;
|
||||||
|
// ---cut---
|
||||||
// Subscribe to a FileStream by ID
|
// Subscribe to a FileStream by ID
|
||||||
const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => {
|
const unsubscribe = FileStream.subscribe(fileStreamId, (fileStream: FileStream) => {
|
||||||
// Called whenever the FileStream changes
|
// Called whenever the FileStream changes
|
||||||
console.log('FileStream updated');
|
console.log('FileStream updated');
|
||||||
|
|
||||||
// Get current status
|
// Get current status
|
||||||
const chunks = fileStream.getChunks({ allowUnfinished: true });
|
const chunks = fileStream.getChunks({ allowUnfinished: true });
|
||||||
if (chunks) {
|
if (chunks) {
|
||||||
const uploadedBytes = chunks.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
const uploadedBytes = chunks.chunks.reduce((sum: number, chunk: Uint8Array) => sum + chunk.length, 0);
|
||||||
const totalBytes = chunks.totalSizeBytes || 1;
|
const totalBytes = chunks.totalSizeBytes || 1;
|
||||||
const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes));
|
const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes));
|
||||||
|
|
||||||
@@ -320,7 +367,10 @@ const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => {
|
|||||||
If you need to wait for a `FileStream` to be fully synchronized across devices:
|
If you need to wait for a `FileStream` to be fully synchronized across devices:
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```ts
|
```ts twoslash
|
||||||
|
import { FileStream } from "jazz-tools";
|
||||||
|
const fileStream = FileStream.create();
|
||||||
|
// ---cut---
|
||||||
// Wait for the FileStream to be fully synced
|
// Wait for the FileStream to be fully synced
|
||||||
await fileStream.waitForSync({
|
await fileStream.waitForSync({
|
||||||
timeout: 5000 // Optional timeout in ms
|
timeout: 5000 // Optional timeout in ms
|
||||||
|
|||||||
@@ -26,4 +26,8 @@ export const features = {
|
|||||||
clerk: "Clerk auth",
|
clerk: "Clerk auth",
|
||||||
inviteLink: "Invite link",
|
inviteLink: "Invite link",
|
||||||
coFeed: "CoFeed",
|
coFeed: "CoFeed",
|
||||||
|
coRichText: "CoRichText",
|
||||||
|
coPlainText: "CoPlainText",
|
||||||
|
serverWorker: "Server worker",
|
||||||
|
inbox: "Inbox",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import DocsLayout from "@/components/docs/DocsLayout";
|
import DocsLayout from "@/components/docs/DocsLayout";
|
||||||
import { DocNav } from "@/components/docs/DocsNav";
|
import { DocNav } from "@/components/docs/DocsNav";
|
||||||
import { Toc } from "@stefanprobst/rehype-extract-toc";
|
|
||||||
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
|
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
|
||||||
|
import { Toc } from "@stefanprobst/rehype-extract-toc";
|
||||||
|
|
||||||
export async function getMdxSource(framework: string, slugPath?: string) {
|
export async function getMdxSource(framework: string, slugPath?: string) {
|
||||||
// Try to import the framework-specific file first
|
// Try to import the framework-specific file first
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"jazz-react": "link:../../packages/jazz-react",
|
"jazz-react": "link:../../packages/jazz-react",
|
||||||
"jazz-react-auth-clerk": "link:../../packages/jazz-react-auth-clerk",
|
"jazz-react-auth-clerk": "link:../../packages/jazz-react-auth-clerk",
|
||||||
"jazz-react-native": "link:../../packages/jazz-react-native",
|
"jazz-react-native": "link:../../packages/jazz-react-native",
|
||||||
|
"jazz-richtext-prosemirror": "link:../../packages/jazz-richtext-prosemirror",
|
||||||
"jazz-tools": "link:../../packages/jazz-tools",
|
"jazz-tools": "link:../../packages/jazz-tools",
|
||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
"mdast-util-from-markdown": "^2.0.0",
|
||||||
|
|||||||
3
homepage/pnpm-lock.yaml
generated
3
homepage/pnpm-lock.yaml
generated
@@ -265,6 +265,9 @@ importers:
|
|||||||
jazz-react-native:
|
jazz-react-native:
|
||||||
specifier: link:../../packages/jazz-react-native
|
specifier: link:../../packages/jazz-react-native
|
||||||
version: link:../../packages/jazz-react-native
|
version: link:../../packages/jazz-react-native
|
||||||
|
jazz-richtext-prosemirror:
|
||||||
|
specifier: link:../../packages/jazz-richtext-prosemirror
|
||||||
|
version: link:../../packages/jazz-richtext-prosemirror
|
||||||
jazz-tools:
|
jazz-tools:
|
||||||
specifier: link:../../packages/jazz-tools
|
specifier: link:../../packages/jazz-tools
|
||||||
version: link:../../packages/jazz-tools
|
version: link:../../packages/jazz-tools
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# cojson-storage-indexeddb
|
# cojson-storage-indexeddb
|
||||||
|
|
||||||
|
## 0.13.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
- cojson-storage@0.13.17
|
||||||
|
|
||||||
## 0.13.16
|
## 0.13.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cojson-storage-indexeddb",
|
"name": "cojson-storage-indexeddb",
|
||||||
"version": "0.13.16",
|
"version": "0.13.17",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# cojson-storage-sqlite
|
# cojson-storage-sqlite
|
||||||
|
|
||||||
|
## 0.13.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
- cojson-storage@0.13.17
|
||||||
|
|
||||||
## 0.13.16
|
## 0.13.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "cojson-storage-sqlite",
|
"name": "cojson-storage-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.13.16",
|
"version": "0.13.17",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
"cojson": "workspace:0.13.16",
|
"cojson": "workspace:0.13.17",
|
||||||
"cojson-storage": "workspace:*"
|
"cojson-storage": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ test("should sync and load data from storage", async () => {
|
|||||||
"storage -> KNOWN Map sessions: header/1",
|
"storage -> KNOWN Map sessions: header/1",
|
||||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||||
"client -> KNOWN Group sessions: header/3",
|
"client -> KNOWN Group sessions: header/3",
|
||||||
"client -> KNOWN Map sessions: header/1",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -211,7 +210,6 @@ test("should load dependencies correctly (group inheritance)", async () => {
|
|||||||
"storage -> KNOWN Map sessions: header/1",
|
"storage -> KNOWN Map sessions: header/1",
|
||||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||||
"client -> KNOWN Group sessions: header/5",
|
"client -> KNOWN Group sessions: header/5",
|
||||||
"client -> KNOWN Map sessions: header/1",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -315,7 +313,6 @@ test("should recover from data loss", async () => {
|
|||||||
"storage -> KNOWN Map sessions: header/4",
|
"storage -> KNOWN Map sessions: header/4",
|
||||||
"storage -> CONTENT Map header: true new: After: 0 New: 4",
|
"storage -> CONTENT Map header: true new: After: 0 New: 4",
|
||||||
"client -> KNOWN Group sessions: header/3",
|
"client -> KNOWN Group sessions: header/3",
|
||||||
"client -> KNOWN Map sessions: header/4",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# cojson-storage
|
# cojson-storage
|
||||||
|
|
||||||
|
## 0.13.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
|
||||||
## 0.13.16
|
## 0.13.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cojson-storage",
|
"name": "cojson-storage",
|
||||||
"version": "0.13.16",
|
"version": "0.13.17",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# cojson-transport-nodejs-ws
|
# cojson-transport-nodejs-ws
|
||||||
|
|
||||||
|
## 0.13.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [9fb98e2]
|
||||||
|
- Updated dependencies [0b89fad]
|
||||||
|
- cojson@0.13.17
|
||||||
|
|
||||||
## 0.13.16
|
## 0.13.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cojson-transport-ws",
|
"name": "cojson-transport-ws",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.13.16",
|
"version": "0.13.17",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# cojson
|
# cojson
|
||||||
|
|
||||||
|
## 0.13.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 9fb98e2: Resolve CoValue load as soon as the core is available
|
||||||
|
- 0b89fad: Re-introduce incremental processing on RawCoList
|
||||||
|
|
||||||
## 0.13.16
|
## 0.13.16
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "0.13.16",
|
"version": "0.13.17",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
|
|||||||
@@ -33,11 +33,14 @@ export interface RawCoValue {
|
|||||||
*
|
*
|
||||||
* Used internally by `useTelepathicData()` for reactive updates on changes to a `CoValue`. */
|
* Used internally by `useTelepathicData()` for reactive updates on changes to a `CoValue`. */
|
||||||
subscribe(listener: (coValue: this) => void): () => void;
|
subscribe(listener: (coValue: this) => void): () => void;
|
||||||
|
|
||||||
|
totalValidTransactions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RawUnknownCoValue implements RawCoValue {
|
export class RawUnknownCoValue implements RawCoValue {
|
||||||
id: CoID<this>;
|
id: CoID<this>;
|
||||||
core: CoValueCore;
|
core: CoValueCore;
|
||||||
|
totalValidTransactions = 0;
|
||||||
|
|
||||||
constructor(core: CoValueCore) {
|
constructor(core: CoValueCore) {
|
||||||
this.id = core.id as CoID<this>;
|
this.id = core.id as CoID<this>;
|
||||||
|
|||||||
@@ -581,6 +581,7 @@ export class CoValueCore {
|
|||||||
|
|
||||||
getValidSortedTransactions(options?: {
|
getValidSortedTransactions(options?: {
|
||||||
ignorePrivateTransactions: boolean;
|
ignorePrivateTransactions: boolean;
|
||||||
|
knownTransactions: CoValueKnownState["sessions"];
|
||||||
}): DecryptedTransaction[] {
|
}): DecryptedTransaction[] {
|
||||||
const allTransactions = this.getValidTransactions(options);
|
const allTransactions = this.getValidTransactions(options);
|
||||||
|
|
||||||
|
|||||||
@@ -83,15 +83,22 @@ export class CoValueState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCoValue() {
|
async getCoValue() {
|
||||||
|
if (this.core) {
|
||||||
|
return this.core;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.highLevelState === "unavailable") {
|
if (this.highLevelState === "unavailable") {
|
||||||
return "unavailable";
|
return "unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<CoValueCore>((resolve) => {
|
return new Promise<CoValueCore | "unavailable">((resolve) => {
|
||||||
const listener = (state: CoValueState) => {
|
const listener = (state: CoValueState) => {
|
||||||
if (state.core) {
|
if (state.core) {
|
||||||
resolve(state.core);
|
resolve(state.core);
|
||||||
this.removeListener(listener);
|
this.removeListener(listener);
|
||||||
|
} else if (state.highLevelState === "unavailable") {
|
||||||
|
resolve("unavailable");
|
||||||
|
this.removeListener(listener);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,122 +111,87 @@ export class CoValueState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAttempt = async (peersToLoadFrom: PeerState[]) => {
|
const peersToActuallyLoadFrom = [];
|
||||||
const peersToActuallyLoadFrom = [];
|
for (const peer of peers) {
|
||||||
for (const peer of peersToLoadFrom) {
|
const currentState = this.peers.get(peer.id);
|
||||||
const currentState = this.peers.get(peer.id);
|
|
||||||
|
|
||||||
if (currentState?.type === "available") {
|
if (
|
||||||
continue;
|
currentState?.type === "available" ||
|
||||||
}
|
currentState?.type === "pending"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentState?.type === "errored") {
|
if (currentState?.type === "errored") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (currentState?.type === "unavailable") {
|
||||||
currentState?.type === "unavailable" ||
|
if (peer.shouldRetryUnavailableCoValues()) {
|
||||||
currentState?.type === "pending"
|
|
||||||
) {
|
|
||||||
if (peer.shouldRetryUnavailableCoValues()) {
|
|
||||||
this.markPending(peer.id);
|
|
||||||
peersToActuallyLoadFrom.push(peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentState || currentState?.type === "unknown") {
|
|
||||||
this.markPending(peer.id);
|
this.markPending(peer.id);
|
||||||
peersToActuallyLoadFrom.push(peer);
|
peersToActuallyLoadFrom.push(peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const peer of peersToActuallyLoadFrom) {
|
if (!currentState || currentState?.type === "unknown") {
|
||||||
if (peer.closed) {
|
this.markPending(peer.id);
|
||||||
this.markNotFoundInPeer(peer.id);
|
peersToActuallyLoadFrom.push(peer);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
peer.pushOutgoingMessage({
|
|
||||||
action: "load",
|
|
||||||
...(this.core ? this.core.knownState() : emptyKnownState(this.id)),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a very long timeout for storage peers, because under pressure
|
|
||||||
* they may take a long time to consume the messages queue
|
|
||||||
*
|
|
||||||
* TODO: Track errors on storage and do not rely on timeout
|
|
||||||
*/
|
|
||||||
const timeoutDuration =
|
|
||||||
peer.role === "storage"
|
|
||||||
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
|
|
||||||
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
|
||||||
|
|
||||||
const waitingForPeer = new Promise<void>((resolve) => {
|
|
||||||
const markNotFound = () => {
|
|
||||||
if (this.peers.get(peer.id)?.type === "pending") {
|
|
||||||
this.markNotFoundInPeer(peer.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeout = setTimeout(markNotFound, timeoutDuration);
|
|
||||||
const removeCloseListener = peer.addCloseListener(markNotFound);
|
|
||||||
|
|
||||||
const listener = (state: CoValueState) => {
|
|
||||||
const peerState = state.peers.get(peer.id);
|
|
||||||
if (
|
|
||||||
state.isAvailable() || // might have become available from another peer e.g. through handleNewContent
|
|
||||||
peerState?.type === "available" ||
|
|
||||||
peerState?.type === "errored" ||
|
|
||||||
peerState?.type === "unavailable"
|
|
||||||
) {
|
|
||||||
state.removeListener(listener);
|
|
||||||
removeCloseListener();
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addListener(listener);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitingForPeer;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
await loadAttempt(peers);
|
|
||||||
|
|
||||||
if (this.isAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry loading from peers that have the retry flag enabled
|
for (const peer of peersToActuallyLoadFrom) {
|
||||||
const peersWithRetry = peers.filter((p) =>
|
if (peer.closed) {
|
||||||
p.shouldRetryUnavailableCoValues(),
|
this.markNotFoundInPeer(peer.id);
|
||||||
);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.pushOutgoingMessage({
|
||||||
|
action: "load",
|
||||||
|
...(this.core ? this.core.knownState() : emptyKnownState(this.id)),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a very long timeout for storage peers, because under pressure
|
||||||
|
* they may take a long time to consume the messages queue
|
||||||
|
*
|
||||||
|
* TODO: Track errors on storage and do not rely on timeout
|
||||||
|
*/
|
||||||
|
const timeoutDuration =
|
||||||
|
peer.role === "storage"
|
||||||
|
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
|
||||||
|
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
||||||
|
|
||||||
|
const waitingForPeer = new Promise<void>((resolve) => {
|
||||||
|
const markNotFound = () => {
|
||||||
|
if (this.peers.get(peer.id)?.type === "pending") {
|
||||||
|
this.markNotFoundInPeer(peer.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(markNotFound, timeoutDuration);
|
||||||
|
const removeCloseListener = peer.addCloseListener(markNotFound);
|
||||||
|
|
||||||
if (peersWithRetry.length > 0) {
|
|
||||||
const waitingForCoValue = new Promise<void>((resolve) => {
|
|
||||||
const listener = (state: CoValueState) => {
|
const listener = (state: CoValueState) => {
|
||||||
if (state.isAvailable()) {
|
const peerState = state.peers.get(peer.id);
|
||||||
|
if (
|
||||||
|
state.isAvailable() || // might have become available from another peer e.g. through handleNewContent
|
||||||
|
peerState?.type === "available" ||
|
||||||
|
peerState?.type === "errored" ||
|
||||||
|
peerState?.type === "unavailable"
|
||||||
|
) {
|
||||||
|
state.removeListener(listener);
|
||||||
|
removeCloseListener();
|
||||||
|
clearTimeout(timeout);
|
||||||
resolve();
|
resolve();
|
||||||
this.removeListener(listener);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.addListener(listener);
|
this.addListener(listener);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We want to exit early if the coValue becomes available in between the retries
|
await waitingForPeer;
|
||||||
await Promise.race([
|
|
||||||
waitingForCoValue,
|
|
||||||
runWithRetry(
|
|
||||||
() => loadAttempt(peersWithRetry),
|
|
||||||
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,30 +243,3 @@ export class CoValueState {
|
|||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runWithRetry<T>(fn: () => Promise<T>, maxRetries: number) {
|
|
||||||
let retries = 1;
|
|
||||||
|
|
||||||
while (retries < maxRetries) {
|
|
||||||
/**
|
|
||||||
* With maxRetries of 5 we should wait:
|
|
||||||
* 300ms
|
|
||||||
* 900ms
|
|
||||||
* 2700ms
|
|
||||||
* 8100ms
|
|
||||||
*/
|
|
||||||
await sleep(3 ** retries * 100);
|
|
||||||
|
|
||||||
const result = await fn();
|
|
||||||
|
|
||||||
if (result === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
retries++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CoID, RawCoValue } from "../coValue.js";
|
|||||||
import { CoValueCore } from "../coValueCore.js";
|
import { CoValueCore } from "../coValueCore.js";
|
||||||
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
import { AgentID, SessionID, TransactionID } from "../ids.js";
|
||||||
import { JsonObject, JsonValue } from "../jsonValue.js";
|
import { JsonObject, JsonValue } from "../jsonValue.js";
|
||||||
|
import { CoValueKnownState } from "../sync.js";
|
||||||
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
||||||
import { isCoValue } from "../typeUtils/isCoValue.js";
|
import { isCoValue } from "../typeUtils/isCoValue.js";
|
||||||
import { RawAccountID } from "./account.js";
|
import { RawAccountID } from "./account.js";
|
||||||
@@ -41,7 +42,7 @@ type DeletionEntry = {
|
|||||||
deletionID: OpID;
|
deletionID: OpID;
|
||||||
} & DeletionOpPayload;
|
} & DeletionOpPayload;
|
||||||
|
|
||||||
export class RawCoListView<
|
export class RawCoList<
|
||||||
Item extends JsonValue = JsonValue,
|
Item extends JsonValue = JsonValue,
|
||||||
Meta extends JsonObject | null = null,
|
Meta extends JsonObject | null = null,
|
||||||
> implements RawCoValue
|
> implements RawCoValue
|
||||||
@@ -81,26 +82,52 @@ export class RawCoListView<
|
|||||||
madeAt: number;
|
madeAt: number;
|
||||||
opID: OpID;
|
opID: OpID;
|
||||||
}[];
|
}[];
|
||||||
|
/** @internal */
|
||||||
|
totalValidTransactions = 0;
|
||||||
|
knownTransactions: CoValueKnownState["sessions"] = {};
|
||||||
|
lastValidTransaction: number | undefined;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
constructor(core: CoValueCore) {
|
constructor(core: CoValueCore) {
|
||||||
this.id = core.id as CoID<this>;
|
this.id = core.id as CoID<this>;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.afterStart = [];
|
|
||||||
this.beforeEnd = [];
|
|
||||||
this.insertions = {};
|
|
||||||
this.deletionsByInsertion = {};
|
|
||||||
|
|
||||||
this.insertions = {};
|
this.insertions = {};
|
||||||
this.deletionsByInsertion = {};
|
this.deletionsByInsertion = {};
|
||||||
this.afterStart = [];
|
this.afterStart = [];
|
||||||
this.beforeEnd = [];
|
this.beforeEnd = [];
|
||||||
|
this.knownTransactions = {};
|
||||||
|
|
||||||
|
this.processNewTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
processNewTransactions() {
|
||||||
|
const transactions = this.core.getValidSortedTransactions({
|
||||||
|
ignorePrivateTransactions: false,
|
||||||
|
knownTransactions: this.knownTransactions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalValidTransactions += transactions.length;
|
||||||
|
let lastValidTransaction: number | undefined = undefined;
|
||||||
|
let oldestValidTransaction: number | undefined = undefined;
|
||||||
|
this._cachedEntries = undefined;
|
||||||
|
|
||||||
|
for (const { txID, changes, madeAt } of transactions) {
|
||||||
|
lastValidTransaction = Math.max(lastValidTransaction ?? 0, madeAt);
|
||||||
|
oldestValidTransaction = Math.min(
|
||||||
|
oldestValidTransaction ?? Infinity,
|
||||||
|
madeAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.knownTransactions[txID.sessionID] = Math.max(
|
||||||
|
this.knownTransactions[txID.sessionID] ?? 0,
|
||||||
|
txID.txIndex,
|
||||||
|
);
|
||||||
|
|
||||||
for (const {
|
|
||||||
txID,
|
|
||||||
changes,
|
|
||||||
madeAt,
|
|
||||||
} of this.core.getValidSortedTransactions()) {
|
|
||||||
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
||||||
const change = changeUntyped as ListOpPayload<Item>;
|
const change = changeUntyped as ListOpPayload<Item>;
|
||||||
|
|
||||||
@@ -193,6 +220,16 @@ export class RawCoListView<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastValidTransaction &&
|
||||||
|
oldestValidTransaction &&
|
||||||
|
oldestValidTransaction < this.lastValidTransaction
|
||||||
|
) {
|
||||||
|
this.rebuildFromCore();
|
||||||
|
} else {
|
||||||
|
this.lastValidTransaction = lastValidTransaction;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @category 6. Meta */
|
/** @category 6. Meta */
|
||||||
@@ -407,15 +444,7 @@ export class RawCoListView<
|
|||||||
listener(content as this);
|
listener(content as this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class RawCoList<
|
|
||||||
Item extends JsonValue = JsonValue,
|
|
||||||
Meta extends JsonObject | null = JsonObject | null,
|
|
||||||
>
|
|
||||||
extends RawCoListView<Item, Meta>
|
|
||||||
implements RawCoValue
|
|
||||||
{
|
|
||||||
/** Appends `item` after the item currently at index `after`.
|
/** Appends `item` after the item currently at index `after`.
|
||||||
*
|
*
|
||||||
* If `privacy` is `"private"` **(default)**, `item` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
* If `privacy` is `"private"` **(default)**, `item` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
|
||||||
@@ -480,8 +509,7 @@ export class RawCoList<
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.core.makeTransaction(changes, privacy);
|
this.core.makeTransaction(changes, privacy);
|
||||||
|
this.processNewTransactions();
|
||||||
this.rebuildFromCore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -528,7 +556,7 @@ export class RawCoList<
|
|||||||
privacy,
|
privacy,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.rebuildFromCore();
|
this.processNewTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deletes the item at index `at`.
|
/** Deletes the item at index `at`.
|
||||||
@@ -555,7 +583,7 @@ export class RawCoList<
|
|||||||
privacy,
|
privacy,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.rebuildFromCore();
|
this.processNewTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(
|
replace(
|
||||||
@@ -583,7 +611,7 @@ export class RawCoList<
|
|||||||
],
|
],
|
||||||
privacy,
|
privacy,
|
||||||
);
|
);
|
||||||
this.rebuildFromCore();
|
this.processNewTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export class RawCoMapView<
|
|||||||
/** @category 6. Meta */
|
/** @category 6. Meta */
|
||||||
readonly _shape!: Shape;
|
readonly _shape!: Shape;
|
||||||
|
|
||||||
|
totalValidTransactions = 0;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
constructor(
|
constructor(
|
||||||
core: CoValueCore,
|
core: CoValueCore,
|
||||||
@@ -133,6 +135,8 @@ export class RawCoMapView<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.totalValidTransactions += newValidTransactions.length;
|
||||||
|
|
||||||
for (const entries of changedEntries.values()) {
|
for (const entries of changedEntries.values()) {
|
||||||
entries.sort(this.core.compareTransactions);
|
entries.sort(this.core.compareTransactions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ export class RawCoPlainText<
|
|||||||
idx = nextIdx;
|
idx = nextIdx;
|
||||||
}
|
}
|
||||||
this.core.makeTransaction(ops, privacy);
|
this.core.makeTransaction(ops, privacy);
|
||||||
|
this.processNewTransactions();
|
||||||
this.rebuildFromCore();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export class RawCoStreamView<
|
|||||||
};
|
};
|
||||||
/** @internal */
|
/** @internal */
|
||||||
knownTransactions: CoValueKnownState["sessions"];
|
knownTransactions: CoValueKnownState["sessions"];
|
||||||
|
totalValidTransactions = 0;
|
||||||
readonly _item!: Item;
|
readonly _item!: Item;
|
||||||
|
|
||||||
constructor(core: CoValueCore) {
|
constructor(core: CoValueCore) {
|
||||||
@@ -96,7 +97,7 @@ export class RawCoStreamView<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
protected processNewTransactions() {
|
processNewTransactions() {
|
||||||
const changeEntries = new Set<CoStreamItem<Item>[]>();
|
const changeEntries = new Set<CoStreamItem<Item>[]>();
|
||||||
|
|
||||||
const newValidTransactions = this.core.getValidTransactions({
|
const newValidTransactions = this.core.getValidTransactions({
|
||||||
@@ -109,6 +110,7 @@ export class RawCoStreamView<
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const { txID, madeAt, changes } of newValidTransactions) {
|
for (const { txID, madeAt, changes } of newValidTransactions) {
|
||||||
|
this.totalValidTransactions++;
|
||||||
for (const changeUntyped of changes) {
|
for (const changeUntyped of changes) {
|
||||||
const change = changeUntyped as Item;
|
const change = changeUntyped as Item;
|
||||||
let entries = this.items[txID.sessionID];
|
let entries = this.items[txID.sessionID];
|
||||||
|
|||||||
@@ -266,29 +266,40 @@ export class LocalNode {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = this.coValuesStore.get(id);
|
let retries = 0;
|
||||||
|
|
||||||
if (
|
while (true) {
|
||||||
entry.highLevelState === "unknown" ||
|
const entry = this.coValuesStore.get(id);
|
||||||
entry.highLevelState === "unavailable"
|
|
||||||
) {
|
|
||||||
const peers =
|
|
||||||
this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
|
|
||||||
|
|
||||||
if (peers.length === 0) {
|
if (
|
||||||
return "unavailable";
|
entry.highLevelState === "unknown" ||
|
||||||
|
entry.highLevelState === "unavailable"
|
||||||
|
) {
|
||||||
|
const peers =
|
||||||
|
this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
|
||||||
|
|
||||||
|
if (peers.length === 0) {
|
||||||
|
return "unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.loadFromPeers(peers).catch((e) => {
|
||||||
|
logger.error("Error loading from peers", {
|
||||||
|
id,
|
||||||
|
err: e,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await entry.loadFromPeers(peers).catch((e) => {
|
const result = await entry.getCoValue();
|
||||||
logger.error("Error loading from peers", {
|
|
||||||
id,
|
|
||||||
err: e,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: What if the loading fails because in the previous loadCoValueCore call the Peer with the covalue was skipped?
|
if (result !== "unavailable" || retries >= 1) {
|
||||||
return entry.getCoValue();
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { expect, test } from "vitest";
|
import { beforeEach, expect, test } from "vitest";
|
||||||
import { expectList } from "../coValue.js";
|
import { expectList } from "../coValue.js";
|
||||||
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||||
import { LocalNode } from "../localNode.js";
|
import { LocalNode } from "../localNode.js";
|
||||||
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
import {
|
||||||
|
loadCoValueOrFail,
|
||||||
|
randomAnonymousAccountAndSessionID,
|
||||||
|
setupTestNode,
|
||||||
|
waitFor,
|
||||||
|
} from "./testUtils.js";
|
||||||
|
|
||||||
const Crypto = await WasmCrypto.create();
|
const Crypto = await WasmCrypto.create();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
setupTestNode({ isSyncServer: true });
|
||||||
|
});
|
||||||
|
|
||||||
test("Empty CoList works", () => {
|
test("Empty CoList works", () => {
|
||||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||||
|
|
||||||
@@ -221,3 +230,176 @@ test("Items prepended to start appear with latest first", () => {
|
|||||||
|
|
||||||
expect(content.toJSON()).toEqual(["third", "second", "first"]);
|
expect(content.toJSON()).toEqual(["third", "second", "first"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mixing prepend and append", () => {
|
||||||
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||||
|
|
||||||
|
const coValue = node.createCoValue({
|
||||||
|
type: "colist",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: null,
|
||||||
|
...Crypto.createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = expectList(coValue.getCurrentContent());
|
||||||
|
|
||||||
|
list.append(2, undefined, "trusting");
|
||||||
|
list.prepend(1, undefined, "trusting");
|
||||||
|
list.append(3, undefined, "trusting");
|
||||||
|
|
||||||
|
expect(list.toJSON()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Items appended to start", () => {
|
||||||
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||||
|
|
||||||
|
const coValue = node.createCoValue({
|
||||||
|
type: "colist",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: null,
|
||||||
|
...Crypto.createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = expectList(coValue.getCurrentContent());
|
||||||
|
|
||||||
|
content.append("first", 0, "trusting");
|
||||||
|
content.append("second", 0, "trusting");
|
||||||
|
content.append("third", 0, "trusting");
|
||||||
|
|
||||||
|
// This result is correct because "third" is appended after "first"
|
||||||
|
// Using the Array methods this would be the same as doing content.splice(1, 0, "third")
|
||||||
|
expect(content.toJSON()).toEqual(["first", "third", "second"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("syncing appends with an older timestamp", async () => {
|
||||||
|
const client = setupTestNode({
|
||||||
|
connected: true,
|
||||||
|
});
|
||||||
|
const otherClient = setupTestNode({});
|
||||||
|
|
||||||
|
const otherClientConnection = otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
const coValue = client.node.createCoValue({
|
||||||
|
type: "colist",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: null,
|
||||||
|
...Crypto.createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = expectList(coValue.getCurrentContent());
|
||||||
|
|
||||||
|
list.append(1, undefined, "trusting");
|
||||||
|
list.append(2, undefined, "trusting");
|
||||||
|
|
||||||
|
const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
|
||||||
|
|
||||||
|
otherClientConnection.peerState.gracefulShutdown();
|
||||||
|
|
||||||
|
listOnOtherClient.append(3, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
list.append(4, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
listOnOtherClient.append(5, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
list.append(6, undefined, "trusting");
|
||||||
|
|
||||||
|
otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(list.toJSON()).toEqual([1, 2, 4, 6, 3, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listOnOtherClient.toJSON()).toEqual(list.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("syncing prepends with an older timestamp", async () => {
|
||||||
|
const client = setupTestNode({
|
||||||
|
connected: true,
|
||||||
|
});
|
||||||
|
const otherClient = setupTestNode({});
|
||||||
|
|
||||||
|
const otherClientConnection = otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
const coValue = client.node.createCoValue({
|
||||||
|
type: "colist",
|
||||||
|
ruleset: { type: "unsafeAllowAll" },
|
||||||
|
meta: null,
|
||||||
|
...Crypto.createdNowUnique(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = expectList(coValue.getCurrentContent());
|
||||||
|
|
||||||
|
list.prepend(1, undefined, "trusting");
|
||||||
|
list.prepend(2, undefined, "trusting");
|
||||||
|
|
||||||
|
const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
|
||||||
|
|
||||||
|
otherClientConnection.peerState.gracefulShutdown();
|
||||||
|
|
||||||
|
listOnOtherClient.prepend(3, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
list.prepend(4, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
listOnOtherClient.prepend(5, undefined, "trusting");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
list.prepend(6, undefined, "trusting");
|
||||||
|
|
||||||
|
otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(list.toJSON()).toEqual([6, 4, 5, 3, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listOnOtherClient.toJSON()).toEqual(list.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("totalValidTransactions should return the number of valid transactions processed", async () => {
|
||||||
|
const client = setupTestNode({
|
||||||
|
connected: true,
|
||||||
|
});
|
||||||
|
const otherClient = setupTestNode({});
|
||||||
|
|
||||||
|
const otherClientConnection = otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
const group = client.node.createGroup();
|
||||||
|
group.addMember("everyone", "reader");
|
||||||
|
|
||||||
|
const list = group.createList([1, 2]);
|
||||||
|
|
||||||
|
const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
|
||||||
|
|
||||||
|
otherClientConnection.peerState.gracefulShutdown();
|
||||||
|
|
||||||
|
group.addMember("everyone", "writer");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
listOnOtherClient.append(3, undefined, "trusting");
|
||||||
|
|
||||||
|
expect(listOnOtherClient.toJSON()).toEqual([1, 2]);
|
||||||
|
expect(listOnOtherClient.totalValidTransactions).toEqual(1);
|
||||||
|
|
||||||
|
otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(listOnOtherClient.core.getCurrentContent().toJSON()).toEqual([
|
||||||
|
1, 2, 3,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
listOnOtherClient.core.getCurrentContent().totalValidTransactions,
|
||||||
|
).toEqual(2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { expect, test } from "vitest";
|
import { beforeEach, expect, test } from "vitest";
|
||||||
import { expectMap } from "../coValue.js";
|
import { expectMap } from "../coValue.js";
|
||||||
import { operationToEditEntry } from "../coValues/coMap.js";
|
import { operationToEditEntry } from "../coValues/coMap.js";
|
||||||
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||||
import { LocalNode } from "../localNode.js";
|
import { LocalNode } from "../localNode.js";
|
||||||
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
||||||
import { hotSleep, randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
import {
|
||||||
|
hotSleep,
|
||||||
|
loadCoValueOrFail,
|
||||||
|
randomAnonymousAccountAndSessionID,
|
||||||
|
setupTestNode,
|
||||||
|
waitFor,
|
||||||
|
} from "./testUtils.js";
|
||||||
|
|
||||||
const Crypto = await WasmCrypto.create();
|
const Crypto = await WasmCrypto.create();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
setupTestNode({ isSyncServer: true });
|
||||||
|
});
|
||||||
|
|
||||||
test("Empty CoMap works", () => {
|
test("Empty CoMap works", () => {
|
||||||
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
||||||
|
|
||||||
@@ -207,3 +217,45 @@ test("Can set items in bulk with assign", () => {
|
|||||||
key3: "assign3",
|
key3: "assign3",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("totalValidTransactions should return the number of valid transactions processed", async () => {
|
||||||
|
const client = setupTestNode({
|
||||||
|
connected: true,
|
||||||
|
});
|
||||||
|
const otherClient = setupTestNode({});
|
||||||
|
|
||||||
|
const otherClientConnection = otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
const group = client.node.createGroup();
|
||||||
|
group.addMember("everyone", "reader");
|
||||||
|
|
||||||
|
const map = group.createMap({ fromClient: true });
|
||||||
|
|
||||||
|
const mapOnOtherClient = await loadCoValueOrFail(otherClient.node, map.id);
|
||||||
|
|
||||||
|
otherClientConnection.peerState.gracefulShutdown();
|
||||||
|
|
||||||
|
group.addMember("everyone", "writer");
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
mapOnOtherClient.set("fromOtherClient", true, "trusting");
|
||||||
|
|
||||||
|
expect(mapOnOtherClient.totalValidTransactions).toEqual(1);
|
||||||
|
expect(mapOnOtherClient.toJSON()).toEqual({
|
||||||
|
fromClient: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
otherClient.connectToSyncServer();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mapOnOtherClient.core.getCurrentContent().toJSON()).toEqual({
|
||||||
|
fromClient: true,
|
||||||
|
fromOtherClient: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mapOnOtherClient.core.getCurrentContent().totalValidTransactions,
|
||||||
|
).toEqual(2);
|
||||||
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user