Compare commits

..

50 Commits

Author SHA1 Message Date
pax
2ae0b8df0d Merge pull request #2055 from garden-co/changeset-release/main
Version Packages
2025-05-01 16:23:16 +03:00
github-actions[bot]
77dc51d466 Version Packages 2025-05-01 13:19:08 +00:00
pax
bd645db4cc Merge pull request #2071 from garden-co/create-jazz-app-git-flag
create-jazz-app --git flag
2025-05-01 16:17:01 +03:00
pax-k
af46c68a4a chore: changest 2025-05-01 16:15:06 +03:00
pax-k
fb58cb9299 feat(create-jazz-app): option to pass git init boolean flag 2025-05-01 16:11:32 +03:00
James Vickery
63fb80e50d Merge pull request #2069 from garden-co/jmsv/jazz-richtext-prosemirror-custom-schema
createJazzPlugin support custom ProseMirror schema
2025-05-01 11:56:35 +01:00
James Vickery
133b8abcbe createJazzPlugin support custom ProseMirror schema 2025-05-01 10:35:16 +01:00
Guido D'Orsi
6209bd2285 Merge pull request #2057 from garden-co/feat/resolve-load-earlier
feat: resolve load earlier and move the retry in LocalNode
2025-04-30 16:25:53 +02:00
Benjamin S. Leveritt
d8d1addf2b Merge pull request #2056 from garden-co/2049-show-how-to-supply-owner-in-filestream-docs
2049-show-how-to-supply-owner-in-filestream-docs
2025-04-30 15:13:49 +01:00
Guido D'Orsi
937a34c76e test(prosemirror): skip failing test 2025-04-30 16:13:42 +02:00
Guido D'Orsi
15996ced64 Merge remote-tracking branch 'origin/main' into feat/resolve-load-earlier 2025-04-30 15:57:38 +02:00
Guido D'Orsi
9fb98e2114 feat: resolve load earlier and move the retry in LocalNode 2025-04-30 15:45:52 +02:00
Benjamin S. Leveritt
f55f779ea1 Adds owner to FileStream examples
Adds twoslash too
2025-04-30 14:36:26 +01:00
Benjamin S. Leveritt
18c98fc3f5 Merge pull request #2053 from garden-co/2052-match-filestreamsubscribe-signatures-with-other-covalues
Update FileStream.subscribe signature to allow omitting options
2025-04-30 14:05:57 +01:00
Guido D'Orsi
41b286b672 Merge pull request #2051 from garden-co/feat/incremental-processing
feat(colist): re-introduce incremental processing
2025-04-30 14:51:25 +02:00
Benjamin S. Leveritt
ba944c20ed Update subscribe signature + tests 2025-04-30 13:28:07 +01:00
Guido D'Orsi
0b89fadfdd feat(colist): re-introduce incremental processing 2025-04-30 13:12:43 +02:00
Guido D'Orsi
1e50cebf55 test(coList): add failing tests on the append operation 2025-04-30 11:33:52 +02:00
Trisha Lim
ca8c5c0b02 Merge pull request #2010 from garden-co/docs/update-examples-page
update examples page
2025-04-30 09:33:15 +01:00
Benjamin S. Leveritt
a0aa261cab Merge pull request #1998 from garden-co/1962-add-cotext-docs
Adds CoText doc
2025-04-30 08:16:11 +01:00
Benjamin S. Leveritt
5d3d11e87c Merge pull request #2016 from garden-co/1862-react-provider-doc
1862-react-provider-doc
2025-04-30 07:01:09 +01:00
Benjamin S. Leveritt
4a9ed21ea2 Fix links 2025-04-29 20:11:22 +01:00
Guido D'Orsi
2ddfc9d92b Merge pull request #2017 from garden-co/changeset-release/main
Version Packages
2025-04-29 18:43:06 +02:00
github-actions[bot]
a032fda936 Version Packages 2025-04-29 16:34:56 +00:00
Guido D'Orsi
c6fb8dc845 fix: handle null values on msg.id 2025-04-29 18:32:08 +02:00
Trisha Lim
69499e3965 lint 2025-04-29 11:56:01 +01:00
Trisha Lim
d67ced14c4 Merge pull request #1988 from garden-co/tobiaslins-patch-1
[expo] Fix CSS import when using `create-jazz-app`
2025-04-29 11:01:16 +01:00
Trisha Lim
95ae69ead2 add coplaintext to rich text example card 2025-04-29 10:07:44 +01:00
Trisha Lim
4170f13858 jazz-paper-scissors: move api key, add lint, change demo url 2025-04-29 10:07:44 +01:00
Trisha Lim
45e4a77afb lint fix 2025-04-29 10:07:44 +01:00
Trisha Lim
603538e255 add jazz-paper-scissors to examples page 2025-04-29 10:07:44 +01:00
Trisha Lim
afb49f3666 update richtext readme 2025-04-29 10:07:44 +01:00
Trisha Lim
c6de2ce8b8 add richtext example to examples page 2025-04-29 10:07:44 +01:00
Benjamin S. Leveritt
cdc4229df7 Add example app 2025-04-28 22:36:09 +01:00
Benjamin S. Leveritt
fa19f7471f Fix component name 2025-04-28 21:54:18 +01:00
Benjamin S. Leveritt
75f3af2cc1 Adds more config options 2025-04-28 21:48:32 +01:00
Benjamin S. Leveritt
5ae77ee57e Adds React Provider doc 2025-04-28 17:39:35 +01:00
Guido D'Orsi
88ea30a6f8 Merge pull request #2013 from garden-co/changeset-release/main
Version Packages
2025-04-28 14:12:53 +02:00
github-actions[bot]
f4cbe395d5 Version Packages 2025-04-28 12:09:21 +00:00
Guido D'Orsi
c59fb5dc1f fix: complete the incremental view revert 2025-04-28 14:07:15 +02:00
Guido D'Orsi
c712ef28e8 fix(coList): revert incremental processing 2025-04-28 13:20:09 +02:00
Guido D'Orsi
c62a4a1c69 Revert "perf(colist): process the content incrementally"
This reverts commit e05dff9c32.
2025-04-28 13:15:56 +02:00
Benjamin S. Leveritt
87a7cf202f Tweak copy 2025-04-28 11:39:27 +01:00
Benjamin S. Leveritt
9a9b424ff2 Add note about co.strings 2025-04-28 11:39:27 +01:00
Benjamin S. Leveritt
dfe6146aa3 Add Vue example 2025-04-28 11:39:27 +01:00
Benjamin S. Leveritt
8b26728914 Refinements 2025-04-28 11:39:27 +01:00
Benjamin S. Leveritt
f5003ac8ec Add more examples 2025-04-28 11:39:26 +01:00
Benjamin S. Leveritt
ee71ba99e2 Adds CoText doc
Closes #1962
2025-04-28 11:39:26 +01:00
pax-k
e1dbab1517 chore: changeset 2025-04-23 22:58:11 +03:00
Tobias Lins
ccc5f89ed7 Fix css path 2025-04-23 21:18:22 +02:00
152 changed files with 3309 additions and 662 deletions

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-expo@0.13.16
- jazz-tools@0.13.16
- jazz-react-native-media-images@0.13.16
## 1.0.107
### Patch Changes
- jazz-expo@0.13.15
- jazz-tools@0.13.15
- jazz-react-native-media-images@0.13.15
## 1.0.106
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-expo-clerk",
"main": "index.js",
"version": "1.0.106",
"version": "1.0.109",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,5 +1,26 @@
# chat-rn-expo
## 1.0.96
### Patch Changes
- jazz-expo@0.13.17
- jazz-tools@0.13.17
## 1.0.95
### Patch Changes
- jazz-expo@0.13.16
- jazz-tools@0.13.16
## 1.0.94
### Patch Changes
- jazz-expo@0.13.15
- jazz-tools@0.13.15
## 1.0.93
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn-expo",
"version": "1.0.93",
"version": "1.0.96",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,5 +1,36 @@
# 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
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
- cojson-transport-ws@0.13.16
- jazz-react-native@0.13.16
- jazz-tools@0.13.16
## 1.0.102
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-transport-ws@0.13.15
- jazz-react-native@0.13.15
- jazz-tools@0.13.15
## 1.0.101
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.101",
"version": "1.0.104",
"main": "index.js",
"scripts": {
"android": "react-native run-android",

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-browser@0.13.16
- jazz-tools@0.13.16
- jazz-vue@0.13.16
## 0.0.86
### Patch Changes
- jazz-browser@0.13.15
- jazz-tools@0.13.15
- jazz-vue@0.13.15
## 0.0.85
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.85",
"version": "0.0.88",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-inspector@0.13.16
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.184
### Patch Changes
- jazz-inspector@0.13.15
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.183
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.183",
"version": "0.0.186",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-react@0.13.16
- jazz-react-auth-clerk@0.13.16
- jazz-tools@0.13.16
## 0.0.83
### Patch Changes
- jazz-react@0.13.15
- jazz-react-auth-clerk@0.13.15
- jazz-tools@0.13.15
## 0.0.82
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.82",
"version": "0.0.85",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# file-share-svelte
## 0.0.68
### Patch Changes
- jazz-svelte@0.13.17
- jazz-tools@0.13.17
## 0.0.67
### Patch Changes
- jazz-svelte@0.13.16
- jazz-tools@0.13.16
## 0.0.66
### Patch Changes
- jazz-svelte@0.13.15
- jazz-tools@0.13.15
## 0.0.65
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.65",
"version": "0.0.68",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-inspector@0.13.16
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.23
### Patch Changes
- jazz-inspector@0.13.15
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.22
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "filestream",
"private": true,
"version": "0.0.22",
"version": "0.0.25",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# form
## 0.1.26
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.1.25
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.1.24
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.1.23
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.1.23",
"version": "0.1.26",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# image-upload
## 0.0.82
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.81
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.80
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.79
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.79",
"version": "0.0.82",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,33 @@
# 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
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
- cojson-transport-ws@0.13.16
- jazz-inspector@0.13.16
## 0.0.134
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-transport-ws@0.13.15
- jazz-inspector@0.13.15
## 0.0.133
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.133",
"version": "0.0.136",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -8,7 +8,9 @@
"dev:worker": "tsx --watch --env-file=.env ./src/worker.ts",
"build": "vite build && tsc",
"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": {
"@radix-ui/react-label": "^2.1.2",

View File

@@ -0,0 +1 @@
export const apiKey = "jazz-paper-scissors@garden.co";

View File

@@ -3,6 +3,7 @@ import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { apiKey } from "@/apiKey.ts";
import { JazzProvider } from "jazz-react";
import { App } from "./app";
@@ -13,7 +14,7 @@ if (rootElement && !rootElement.innerHTML) {
<StrictMode>
<JazzProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
<JazzInspector />

View File

@@ -12,9 +12,9 @@
import { Route as rootRoute } from "./routes/__root";
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 AuthenticatedWaitingRoomWaitingRoomIdImport } from "./routes/_authenticated/waiting-room.$waitingRoomId";
import { Route as IndexImport } from "./routes/index";
// Create/Update Routes

View File

@@ -1,5 +1,26 @@
# multi-cursors
## 0.0.78
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.77
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.76
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.75
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multi-cursors",
"private": true,
"version": "0.0.75",
"version": "0.0.78",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-react@0.13.16
- jazz-react-auth-clerk@0.13.16
- jazz-tools@0.13.16
## 0.0.24
### Patch Changes
- jazz-react@0.13.15
- jazz-react-auth-clerk@0.13.15
- jazz-tools@0.13.15
## 0.0.23
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multiauth",
"private": true,
"version": "0.0.23",
"version": "0.0.26",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-inspector@0.13.16
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.105
### Patch Changes
- jazz-inspector@0.13.15
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.104
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.104",
"version": "0.0.107",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# organization
## 0.0.78
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.77
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.76
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.75
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.75",
"version": "0.0.78",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# passkey-svelte
## 0.0.72
### Patch Changes
- jazz-svelte@0.13.17
- jazz-tools@0.13.17
## 0.0.71
### Patch Changes
- jazz-svelte@0.13.16
- jazz-tools@0.13.16
## 0.0.70
### Patch Changes
- jazz-svelte@0.13.15
- jazz-tools@0.13.15
## 0.0.69
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.69",
"version": "0.0.72",
"type": "module",
"private": true,
"scripts": {

View File

@@ -1,5 +1,26 @@
# minimal-auth-passkey
## 0.0.83
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.82
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.81
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.80
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.80",
"version": "0.0.83",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# passphrase
## 0.0.80
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.79
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.78
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.77
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passphrase",
"private": true,
"version": "0.0.77",
"version": "0.0.80",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# jazz-password-manager
## 0.0.104
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.103
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.102
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.101
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.101",
"version": "0.0.104",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# jazz-example-pets
## 0.0.202
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.201
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.200
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.199
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.199",
"version": "0.0.202",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,26 @@
# reactions
## 0.0.82
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.81
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.80
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.79
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.79",
"version": "0.0.82",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,30 @@
# 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
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
- jazz-richtext-prosemirror@0.1.5
## 0.0.70
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
- jazz-richtext-prosemirror@0.1.4
## 0.0.69
### Patch Changes

View File

@@ -2,6 +2,8 @@
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
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
- Real-time collaboration across devices
- 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
npm i
# or
yarn
npx create-jazz-app@latest richtext-app --example richtext
```
Then, run the development server:
Go to the new project directory.
```bash
cd richtext-app
```
Run the dev server.
```bash
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.

View File

@@ -1,7 +1,7 @@
{
"name": "richtext",
"private": true,
"version": "0.0.69",
"version": "0.0.72",
"type": "module",
"scripts": {
"dev": "vite",
@@ -19,6 +19,7 @@
"prosemirror-example-setup": "^1.2.3",
"prosemirror-model": "^1.25.0",
"prosemirror-schema-basic": "^1.2.4",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.39.1",
"react": "18.3.1",

View File

@@ -1,7 +1,9 @@
import { useAccount } from "jazz-react";
import { createJazzPlugin } from "jazz-richtext-prosemirror";
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 { EditorView } from "prosemirror-view";
import { useEffect, useRef } from "react";
@@ -14,6 +16,11 @@ export function Editor() {
useEffect(() => {
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 jazzPlugin = createJazzPlugin(me.profile.bio);

View File

@@ -1,3 +1,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.ProseMirror ul {
@apply list-disc;
}
.ProseMirror ol {
@apply list-decimal;
}

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-browser@0.13.16
- jazz-tools@0.13.16
- jazz-vue@0.13.16
## 0.0.84
### Patch Changes
- jazz-browser@0.13.15
- jazz-tools@0.13.15
- jazz-vue@0.13.15
## 0.0.83
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.83",
"version": "0.0.86",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,26 @@
# jazz-example-todo
## 0.0.201
### Patch Changes
- jazz-react@0.13.17
- jazz-tools@0.13.17
## 0.0.200
### Patch Changes
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.199
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.198
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.198",
"version": "0.0.201",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,29 @@
# 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
### Patch Changes
- jazz-inspector@0.13.16
- jazz-react@0.13.16
- jazz-tools@0.13.16
## 0.0.78
### Patch Changes
- jazz-inspector@0.13.15
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.77
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.77",
"version": "0.0.80",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -2,6 +2,7 @@ import {
AlertTriangleIcon,
ArrowDownIcon,
ArrowRightIcon,
BoldIcon,
BookTextIcon,
BoxIcon,
BracesIcon,
@@ -20,6 +21,7 @@ import {
HashIcon,
ImageIcon,
InfoIcon,
ItalicIcon,
LinkIcon,
LockKeyholeIcon,
type LucideIcon,
@@ -87,6 +89,10 @@ const icons = {
colist: Brackets,
user: UserIcon,
group: UsersIcon,
// text editor icons
bold: BoldIcon,
italic: ItalicIcon,
};
// copied from tailwind line height https://tailwindcss.com/docs/font-size

View File

@@ -1,6 +1,6 @@
import { packages } from "@/content/packages";
import { clsx } from "clsx";
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
import { clsx } from "clsx";
import type { Metadata } from "next";
import Link from "next/link";

View File

@@ -6,11 +6,11 @@ import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
import { SvelteLogo } from "@/components/icons/SvelteLogo";
import { VueLogo } from "@/components/icons/VueLogo";
import { Example, features, tech } from "@/content/example";
import { clsx } from "clsx";
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
import { clsx } from "clsx";
import type { Metadata } from "next";
const title = "Examples";
@@ -198,6 +198,12 @@ const MusicIllustration = () => (
</div>
);
const JazzPaperScissorsIllustration = () => (
<div className="flex flex-col items-center justify-center h-full p-8 text-4xl">
</div>
);
const ImageUploadIllustration = () => (
<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">
@@ -255,21 +261,21 @@ const ReactionsIllustration = () => (
const MultiCursorIllustration = () => (
<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">
<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 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="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 -left-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!
</span>
<div className="absolute -top-10 right-4 text-rose-600 flex items-end gap-1">
@@ -285,6 +291,21 @@ const MultiCursorIllustration = () => (
</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 = () => (
<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">
@@ -434,9 +455,19 @@ const reactExamples: Example[] = [
"Track user presence on a canvas with multiple cursors and out of bounds indicators.",
tech: [tech.react],
features: [features.coFeed],
demoUrl: "https://jazz-multi-cursors.vercel.app",
demoUrl: "https://multi-cursors-demo.jazz.tools",
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",
slug: "pets",
@@ -477,6 +508,16 @@ const reactExamples: Example[] = [
demoUrl: "https://music-demo.jazz.tools",
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",
slug: "clerk",
@@ -511,6 +552,7 @@ const reactExamples: Example[] = [
tech: [tech.react],
features: [features.inviteLink],
illustration: <OrganizationIllustration />,
demoUrl: "https://jazz-organization.vercel.app"
},
{
name: "Version history",
@@ -519,6 +561,7 @@ const reactExamples: Example[] = [
"Track and restore previous versions of your data, and see who made the changes.",
tech: [tech.react],
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.",
tech: [tech.vue],
features: [features.inviteLink],
demoUrl: "https://todo-demo.jazz.tools",
illustration: (
<div className="h-full w-full bg-cover bg-[url('/todo.jpg')] bg-left-bottom"></div>
),

View File

@@ -1,6 +1,6 @@
import LatencyChart from "@/components/LatencyChart";
import { clsx } from "clsx";
import { HeroHeader } from "@garden-co/design-system/src/components/molecules/HeroHeader";
import { clsx } from "clsx";
import type { Metadata } from "next";
import { Fragment } from "react";

View File

@@ -1,11 +1,11 @@
import "./globals.css";
import type { Metadata } from "next";
import { fontClasses } from "@garden-co/design-system/src/fonts";
import { ThemeProvider } from "@/components/ThemeProvider";
import { JazzFooter } from "@/components/footer";
import { marketingCopy } from "@/content/marketingCopy";
import { fontClasses } from "@garden-co/design-system/src/fonts";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next";
const metaTags = {
title: `Jazz - ${marketingCopy.headline}`,

View File

@@ -1,7 +1,7 @@
"use client";
import { track } from "@vercel/analytics";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import { track } from "@vercel/analytics";
export function FakeGetStartedButton({ tier }: { tier: "starter" | "indie" }) {
return (

View File

@@ -1,5 +1,5 @@
import { clsx } from "clsx";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import { clsx } from "clsx";
import {
CircleCheckIcon,
LucideBuilding2,

View File

@@ -1,7 +1,7 @@
"use client";
import { clsx } from "clsx";
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
import { clsx } from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode } from "react";

View File

@@ -1,6 +1,6 @@
import { JazzNav } from "@/components/nav";
import { clsx } from "clsx";
import { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
import { clsx } from "clsx";
export function SideNavLayout({
children,

View File

@@ -2,9 +2,9 @@
import { TableOfContents } from "@/components/docs/TableOfContents";
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 { NavSection } from "@garden-co/design-system/src/components/organisms/Nav";
import { TocEntry } from "@stefanprobst/rehype-extract-toc";
export default function DocsLayout({
children,

View File

@@ -2,7 +2,6 @@
import { Example } from "@/content/example";
import { InterpolateInCode } from "@/mdx-components";
import { DialogDescription } from "@headlessui/react";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import { CodeGroup } from "@garden-co/design-system/src/components/molecules/CodeGroup";
import {
@@ -11,6 +10,7 @@ import {
DialogBody,
DialogTitle,
} from "@garden-co/design-system/src/components/organisms/Dialog";
import { DialogDescription } from "@headlessui/react";
import { useState } from "react";
import CreateJazzApp from "./CreateJazzApp.mdx";

View File

@@ -1,8 +1,8 @@
import { clsx } from "clsx";
import { Card } from "@garden-co/design-system/src/components/atoms/Card";
import { H2 } from "@garden-co/design-system/src/components/atoms/Headings";
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
import { clsx } from "clsx";
import CodeStepAction from "./CodeStepAction.mdx";
import CodeStepCloud from "./CodeStepCloud.mdx";
import CodeStepRender from "./CodeStepRender.mdx";

File diff suppressed because one or more lines are too long

View File

@@ -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.
## 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
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: "signedUp"`: Sync is enabled when the user is authenticated
@@ -197,7 +208,7 @@ function App() {
### 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">
<CodeGroup>

View File

@@ -53,6 +53,7 @@ export const docNavigationItems = [
name: "Providers",
href: "/docs/project-setup/providers",
done: {
react: 100,
"react-native": 100,
"react-native-expo": 100,
},
@@ -168,6 +169,11 @@ export const docNavigationItems = [
href: "/docs/using-covalues/cofeeds",
done: 100,
},
{
name: "CoTexts",
href: "/docs/using-covalues/cotexts",
done: 100,
},
{
name: "FileStreams",
href: "/docs/using-covalues/filestreams",

View 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.

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

View File

@@ -18,7 +18,8 @@ FileStreams provide automatic chunking when using the `createFromBlob` method, t
In your schema, reference FileStreams like any other CoValue:
<CodeGroup>
```ts
```ts twoslash
// schema.ts
import { CoMap, FileStream, co } from "jazz-tools";
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`:
<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
const fileInput = document.querySelector('input[type="file"]');
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
if (file) {
// Create FileStream from user-selected file
const fileStream = await FileStream.createFromBlob(file);
// Or with progress tracking for better UX
const fileWithProgress = await FileStream.createFromBlob(file, {
onProgress: (progress) => {
// progress is a value between 0 and 1
const percent = Math.round(progress * 100);
console.log(`Upload progress: ${percent}%`);
progressBar.style.width = `${percent}%`;
}
});
}
const file = fileInput.files?.[0];
if (!file) return;
// 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, {
onProgress: (progress) => {
// progress is a value between 0 and 1
const percent = Math.round(progress * 100);
console.log(`Upload progress: ${percent}%`);
progressBar.style.width = `${percent}%`;
},
owner: myGroup
});
});
```
</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):
<CodeGroup>
```ts
import { FileStream } from "jazz-tools";
```ts twoslash
import { Group, FileStream } from "jazz-tools";
const myGroup = Group.create();
// ---cut---
// Create a new empty FileStream
const fileStream = FileStream.create();
const fileStream = FileStream.create({ owner: myGroup } );
```
</CodeGroup>
@@ -105,7 +115,10 @@ const teamFileStream = FileStream.create({ owner: teamGroup });
To access the raw binary data and metadata:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
// Get all chunks and metadata
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:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
// Get data even if the stream isn't complete
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`:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
// Convert to a Blob
const blob = fileStream.toBlob();
// Get the filename from the metadata
const filename = fileStream.getChunks()?.fileName;
if (blob) {
// Use with URL.createObjectURL
const url = URL.createObjectURL(blob);
@@ -149,7 +171,7 @@ if (blob) {
// Create a download link
const link = document.createElement('a');
link.href = url;
link.download = fileData?.fileName || 'document.pdf';
link.download = filename || 'document.pdf';
link.click();
// 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:
<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
const blob = await FileStream.loadAsBlob(fileStreamId);
@@ -180,7 +205,10 @@ const partialBlob = await FileStream.loadAsBlob(fileStreamId, {
Check if a `FileStream` is fully synced:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
if (fileStream.isBinaryStreamEnded()) {
console.log('File is completely synced');
} else {
@@ -206,9 +234,12 @@ When creating a `FileStream` manually (not using `createFromBlob`), you need to
Begin by providing metadata about the file:
<CodeGroup>
```ts
```ts twoslash
import { FileStream, Group } from "jazz-tools";
const myGroup = Group.create();
// ---cut---
// Create an empty FileStream
const fileStream = FileStream.create();
const fileStream = FileStream.create({ owner: myGroup });
// Initialize with metadata
fileStream.start({
@@ -224,9 +255,15 @@ fileStream.start({
Add binary data in chunks - this helps with large files and progress tracking:
<CodeGroup>
```ts
// Create a sample Uint8Array (in real apps, this would be file data)
const data = new Uint8Array([...]);
```ts twoslash
import { FileStream } from "jazz-tools";
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)
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:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
// Finalize the upload
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:
<CodeGroup>
```ts
```ts twoslash
import { FileStream, type ID } from "jazz-tools";
const fileStreamId = "co_z123" as ID<FileStream>;
// ---cut---
// Load a FileStream by ID
const fileStream = await FileStream.load(fileStreamId, []);
const fileStream = await FileStream.load(fileStreamId);
if (fileStream) {
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:
<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
const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => {
const unsubscribe = FileStream.subscribe(fileStreamId, (fileStream: FileStream) => {
// Called whenever the FileStream changes
console.log('FileStream updated');
// Get current status
const chunks = fileStream.getChunks({ allowUnfinished: true });
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 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:
<CodeGroup>
```ts
```ts twoslash
import { FileStream } from "jazz-tools";
const fileStream = FileStream.create();
// ---cut---
// Wait for the FileStream to be fully synced
await fileStream.waitForSync({
timeout: 5000 // Optional timeout in ms

View File

@@ -26,4 +26,8 @@ export const features = {
clerk: "Clerk auth",
inviteLink: "Invite link",
coFeed: "CoFeed",
coRichText: "CoRichText",
coPlainText: "CoPlainText",
serverWorker: "Server worker",
inbox: "Inbox",
};

View File

@@ -1,7 +1,7 @@
import DocsLayout from "@/components/docs/DocsLayout";
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 { Toc } from "@stefanprobst/rehype-extract-toc";
export async function getMdxSource(framework: string, slugPath?: string) {
// Try to import the framework-specific file first

View File

@@ -44,6 +44,7 @@
"jazz-react": "link:../../packages/jazz-react",
"jazz-react-auth-clerk": "link:../../packages/jazz-react-auth-clerk",
"jazz-react-native": "link:../../packages/jazz-react-native",
"jazz-richtext-prosemirror": "link:../../packages/jazz-richtext-prosemirror",
"jazz-tools": "link:../../packages/jazz-tools",
"lucide-react": "^0.436.0",
"mdast-util-from-markdown": "^2.0.0",

View File

@@ -265,6 +265,9 @@ importers:
jazz-react-native:
specifier: 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:
specifier: link:../../packages/jazz-tools
version: link:../../packages/jazz-tools

View File

@@ -1,5 +1,30 @@
# 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
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
- cojson-storage@0.13.16
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-storage@0.13.15
## 0.13.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.13.14",
"version": "0.13.17",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,30 @@
# 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
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
- cojson-storage@0.13.16
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-storage@0.13.15
## 0.13.14
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.13.14",
"version": "0.13.17",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.13.14",
"cojson": "workspace:0.13.17",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -121,7 +121,6 @@ test("should sync and load data from storage", async () => {
"storage -> KNOWN Map sessions: header/1",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"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 -> CONTENT Map header: true new: After: 0 New: 1",
"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 -> CONTENT Map header: true new: After: 0 New: 4",
"client -> KNOWN Group sessions: header/3",
"client -> KNOWN Map sessions: header/4",
]
`);
});

View File

@@ -1,5 +1,27 @@
# cojson-storage
## 0.13.17
### Patch Changes
- Updated dependencies [9fb98e2]
- Updated dependencies [0b89fad]
- cojson@0.13.17
## 0.13.16
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
## 0.13.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage",
"version": "0.13.14",
"version": "0.13.17",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,27 @@
# cojson-transport-nodejs-ws
## 0.13.17
### Patch Changes
- Updated dependencies [9fb98e2]
- Updated dependencies [0b89fad]
- cojson@0.13.17
## 0.13.16
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
## 0.13.14
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.13.14",
"version": "0.13.17",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -1,5 +1,24 @@
# 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
### Patch Changes
- c6fb8dc: Handle null values in msg.id
## 0.13.15
### Patch Changes
- c712ef2: Revert the RawCoList incremental processing
## 0.13.14
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.13.14",
"version": "0.13.17",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"typescript": "catalog:"

View File

@@ -33,11 +33,14 @@ export interface RawCoValue {
*
* Used internally by `useTelepathicData()` for reactive updates on changes to a `CoValue`. */
subscribe(listener: (coValue: this) => void): () => void;
totalValidTransactions: number;
}
export class RawUnknownCoValue implements RawCoValue {
id: CoID<this>;
core: CoValueCore;
totalValidTransactions = 0;
constructor(core: CoValueCore) {
this.id = core.id as CoID<this>;

View File

@@ -581,6 +581,7 @@ export class CoValueCore {
getValidSortedTransactions(options?: {
ignorePrivateTransactions: boolean;
knownTransactions: CoValueKnownState["sessions"];
}): DecryptedTransaction[] {
const allTransactions = this.getValidTransactions(options);

View File

@@ -83,15 +83,22 @@ export class CoValueState {
}
async getCoValue() {
if (this.core) {
return this.core;
}
if (this.highLevelState === "unavailable") {
return "unavailable";
}
return new Promise<CoValueCore>((resolve) => {
return new Promise<CoValueCore | "unavailable">((resolve) => {
const listener = (state: CoValueState) => {
if (state.core) {
resolve(state.core);
this.removeListener(listener);
} else if (state.highLevelState === "unavailable") {
resolve("unavailable");
this.removeListener(listener);
}
};
@@ -104,122 +111,87 @@ export class CoValueState {
return;
}
const loadAttempt = async (peersToLoadFrom: PeerState[]) => {
const peersToActuallyLoadFrom = [];
for (const peer of peersToLoadFrom) {
const currentState = this.peers.get(peer.id);
const peersToActuallyLoadFrom = [];
for (const peer of peers) {
const currentState = this.peers.get(peer.id);
if (currentState?.type === "available") {
continue;
}
if (
currentState?.type === "available" ||
currentState?.type === "pending"
) {
continue;
}
if (currentState?.type === "errored") {
continue;
}
if (currentState?.type === "errored") {
continue;
}
if (
currentState?.type === "unavailable" ||
currentState?.type === "pending"
) {
if (peer.shouldRetryUnavailableCoValues()) {
this.markPending(peer.id);
peersToActuallyLoadFrom.push(peer);
}
continue;
}
if (!currentState || currentState?.type === "unknown") {
if (currentState?.type === "unavailable") {
if (peer.shouldRetryUnavailableCoValues()) {
this.markPending(peer.id);
peersToActuallyLoadFrom.push(peer);
}
continue;
}
for (const peer of peersToActuallyLoadFrom) {
if (peer.closed) {
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);
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;
if (!currentState || currentState?.type === "unknown") {
this.markPending(peer.id);
peersToActuallyLoadFrom.push(peer);
}
};
await loadAttempt(peers);
if (this.isAvailable()) {
return;
}
// Retry loading from peers that have the retry flag enabled
const peersWithRetry = peers.filter((p) =>
p.shouldRetryUnavailableCoValues(),
);
for (const peer of peersToActuallyLoadFrom) {
if (peer.closed) {
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) => {
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();
this.removeListener(listener);
}
};
this.addListener(listener);
});
// We want to exit early if the coValue becomes available in between the retries
await Promise.race([
waitingForCoValue,
runWithRetry(
() => loadAttempt(peersWithRetry),
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
),
]);
await waitingForPeer;
}
}
@@ -271,30 +243,3 @@ export class CoValueState {
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));
}

View File

@@ -42,7 +42,7 @@ type DeletionEntry = {
deletionID: OpID;
} & DeletionOpPayload;
export class RawCoListView<
export class RawCoList<
Item extends JsonValue = JsonValue,
Meta extends JsonObject | null = null,
> implements RawCoValue
@@ -83,16 +83,14 @@ export class RawCoListView<
opID: OpID;
}[];
/** @internal */
knownTransactions: CoValueKnownState["sessions"];
totalValidTransactions = 0;
knownTransactions: CoValueKnownState["sessions"] = {};
lastValidTransaction: number | undefined;
/** @internal */
constructor(core: CoValueCore) {
this.id = core.id as CoID<this>;
this.core = core;
this.afterStart = [];
this.beforeEnd = [];
this.insertions = {};
this.deletionsByInsertion = {};
this.insertions = {};
this.deletionsByInsertion = {};
@@ -104,18 +102,32 @@ export class RawCoListView<
}
processNewTransactions() {
const newTransactions = this.core.getValidTransactions({
const transactions = this.core.getValidSortedTransactions({
ignorePrivateTransactions: false,
knownTransactions: this.knownTransactions,
});
if (newTransactions.length === 0) {
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 newTransactions) {
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 [changeIdx, changeUntyped] of changes.entries()) {
const change = changeUntyped as ListOpPayload<Item>;
@@ -207,11 +219,16 @@ export class RawCoListView<
);
}
}
}
this.knownTransactions[txID.sessionID] = Math.max(
this.knownTransactions[txID.sessionID] ?? 0,
txID.txIndex,
);
if (
this.lastValidTransaction &&
oldestValidTransaction &&
oldestValidTransaction < this.lastValidTransaction
) {
this.rebuildFromCore();
} else {
this.lastValidTransaction = lastValidTransaction;
}
}
@@ -427,15 +444,7 @@ export class RawCoListView<
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`.
*
* 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.
@@ -500,7 +509,6 @@ export class RawCoList<
}
this.core.makeTransaction(changes, privacy);
this.processNewTransactions();
}
@@ -605,4 +613,15 @@ export class RawCoList<
);
this.processNewTransactions();
}
/** @internal */
rebuildFromCore() {
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
}
}

View File

@@ -60,6 +60,8 @@ export class RawCoMapView<
/** @category 6. Meta */
readonly _shape!: Shape;
totalValidTransactions = 0;
/** @internal */
constructor(
core: CoValueCore,
@@ -133,6 +135,8 @@ export class RawCoMapView<
}
}
this.totalValidTransactions += newValidTransactions.length;
for (const entries of changedEntries.values()) {
entries.sort(this.core.compareTransactions);
}

View File

@@ -186,7 +186,6 @@ export class RawCoPlainText<
idx = nextIdx;
}
this.core.makeTransaction(ops, privacy);
this.processNewTransactions();
}
}

View File

@@ -56,6 +56,7 @@ export class RawCoStreamView<
};
/** @internal */
knownTransactions: CoValueKnownState["sessions"];
totalValidTransactions = 0;
readonly _item!: Item;
constructor(core: CoValueCore) {
@@ -96,7 +97,7 @@ export class RawCoStreamView<
}
/** @internal */
protected processNewTransactions() {
processNewTransactions() {
const changeEntries = new Set<CoStreamItem<Item>[]>();
const newValidTransactions = this.core.getValidTransactions({
@@ -109,6 +110,7 @@ export class RawCoStreamView<
}
for (const { txID, madeAt, changes } of newValidTransactions) {
this.totalValidTransactions++;
for (const changeUntyped of changes) {
const change = changeUntyped as Item;
let entries = this.items[txID.sessionID];

View File

@@ -266,29 +266,40 @@ export class LocalNode {
});
}
const entry = this.coValuesStore.get(id);
let retries = 0;
if (
entry.highLevelState === "unknown" ||
entry.highLevelState === "unavailable"
) {
const peers =
this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
while (true) {
const entry = this.coValuesStore.get(id);
if (peers.length === 0) {
return "unavailable";
if (
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) => {
logger.error("Error loading from peers", {
id,
err: e,
});
});
}
const result = await entry.getCoValue();
// TODO: What if the loading fails because in the previous loadCoValueCore call the Peer with the covalue was skipped?
return entry.getCoValue();
if (result !== "unavailable" || retries >= 1) {
return result;
}
await new Promise((resolve) => setTimeout(resolve, 300));
retries++;
}
}
/**

View File

@@ -163,13 +163,13 @@ export class SyncManager {
`Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
);
return;
} else if (msg.id === undefined) {
logger.info("Received sync message with undefined id", {
} else if (msg.id === undefined || msg.id === null) {
logger.warn("Received sync message with undefined id", {
msg,
});
return;
} else if (!msg.id.startsWith("co_z")) {
logger.info("Received sync message with invalid id", {
logger.warn("Received sync message with invalid id", {
msg,
});
return;

View File

@@ -1,11 +1,20 @@
import { expect, test } from "vitest";
import { beforeEach, expect, test } from "vitest";
import { expectList } from "../coValue.js";
import { WasmCrypto } from "../crypto/WasmCrypto.js";
import { LocalNode } from "../localNode.js";
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
import {
loadCoValueOrFail,
randomAnonymousAccountAndSessionID,
setupTestNode,
waitFor,
} from "./testUtils.js";
const Crypto = await WasmCrypto.create();
beforeEach(async () => {
setupTestNode({ isSyncServer: true });
});
test("Empty CoList works", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
@@ -222,15 +231,175 @@ test("Items prepended to start appear with latest first", () => {
expect(content.toJSON()).toEqual(["third", "second", "first"]);
});
test("should handle large lists", () => {
test("mixing prepend and append", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const group = node.createGroup();
const coValue = group.createList();
const coValue = node.createCoValue({
type: "colist",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
for (let i = 0; i < 8_000; i++) {
coValue.append(`item ${i}`, undefined, "trusting");
}
const list = expectList(coValue.getCurrentContent());
expect(coValue.toJSON().length).toEqual(8_000);
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);
});

Some files were not shown because too many files have changed in this diff Show More