Compare commits

...

55 Commits

Author SHA1 Message Date
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
Guido D'Orsi
d28ce598e2 Merge pull request #2003 from garden-co/changeset-release/main
Version Packages
2025-04-26 10:24:45 +02:00
github-actions[bot]
14475991c8 Version Packages 2025-04-25 12:39:39 +00:00
Guido D'Orsi
15d9ec4b38 chore: small cleanup on startPeerReconciliation 2025-04-25 14:37:34 +02:00
Guido D'Orsi
f911545ae3 Merge pull request #2006 from garden-co/chore/simplify-coValue-sync
chore: clean up syncCoValue code and remove the peer role
2025-04-25 13:55:00 +02:00
Guido D'Orsi
ad71530cc0 Merge pull request #2005 from garden-co/feat/no-sync-promises
feat(sync): make the incoming messages handling in the sync manager syncronous
2025-04-25 13:54:35 +02:00
Guido D'Orsi
c33c02691f chore: clean up syncCoValue code and remove the peer role 2025-04-25 13:22:02 +02:00
Guido D'Orsi
51c19770a8 feat: remove unused promises in PeerState outgoing queue 2025-04-25 13:08:20 +02:00
Guido D'Orsi
5c2c7d4188 feat(sync): make the incoming messages handling in the sync manager syncronous 2025-04-25 12:36:31 +02:00
Benjamin S. Leveritt
334d27d53d Merge pull request #2002 from garden-co/2001-fix-unused-vars-in-jazz-paper-scissors
Removes unused vars
2025-04-25 08:19:17 +01:00
Benjamin S. Leveritt
a5396a42ce Merge pull request #1989 from garden-co/fix-rn-metro-docs
docs(metro): fix RN docs for metro config
2025-04-24 15:31:12 +01:00
Benjamin S. Leveritt
5cfe38d547 Removes unused vars 2025-04-24 13:43:25 +01:00
Benjamin S. Leveritt
3f7aa34726 Merge pull request #1996 from garden-co/1995-add-ownership-sections-to-all-the-covalue-docs
Add Ownership sections to CoValue docs
2025-04-24 13:11:52 +01:00
Trisha Lim
008750d401 Merge pull request #1997 from garden-co/docs/framework-component
add Framework component to show framework name
2025-04-24 12:27:54 +01:00
Trisha Lim
72708f82ea add Framework component to show framework name 2025-04-24 12:18:24 +01:00
Benjamin S. Leveritt
30f65f1c91 Add Ownership sections to CoValue docs 2025-04-24 12:06:52 +01:00
Meg Culotta
67d55ce0ee update styles to handle mobile devices (#1944)
* update styles to handle mobile devices

* handle twoslash overflow

---------

Co-authored-by: Margaret Culotta <mculotta@Margarets-MacBook-Air.local>
Co-authored-by: Trisha Lim <hello@trishalim.com>
2025-04-24 09:11:36 +01:00
Trisha Lim
e887f37713 Merge pull request #1994 from garden-co/fix/code-copy
fix(docs): exclude twoslash popovers from getting copied
2025-04-24 09:08:55 +01:00
Trisha Lim
82a515d493 fix(docs): exclude twoslash popovers from getting copied 2025-04-24 08:57:00 +01:00
pax-k
bd94012507 chore: changeset 2025-04-23 23:00:22 +03:00
Guido D'Orsi
b675249960 Merge pull request #1990 from garden-co/changeset-release/main
Version Packages
2025-04-23 21:49:44 +02:00
github-actions[bot]
05198e4181 Version Packages 2025-04-23 19:48:47 +00:00
Guido D'Orsi
ec9cb40fa4 fix: remove .every() call on iterator to fix compat issues with React Native 2025-04-23 21:46:27 +02:00
pax-k
dafea6039b docs(metro): fix RN docs for metro config 2025-04-23 22:31:50 +03:00
Guido D'Orsi
fae9b521b8 Merge pull request #1984 from garden-co/changeset-release/main
Version Packages
2025-04-23 19:47:23 +02:00
github-actions[bot]
ec1e2e4539 Version Packages 2025-04-23 17:46:34 +00:00
Guido D'Orsi
9550dcd6e7 Merge pull request #1987 from garden-co/fix/handle-unknown-keys-comap-toJSON
fix: skip non-schema related keys when calling CoMap.toJSON
2025-04-23 19:44:20 +02:00
Guido D'Orsi
4547525579 fix: skip non-schema related keys when calling CoMap.toJSON 2025-04-23 19:31:41 +02:00
Guido D'Orsi
856ba0c1fa Merge pull request #1943 from garden-co/refactor/simplify-covalue-state-anselm
feat: simplify the CoValue loading state management
2025-04-23 18:45:26 +02:00
Guido D'Orsi
29e05c4ad4 chore: changeset 2025-04-23 16:48:04 +02:00
Guido D'Orsi
65719f21a3 chore: changeset 2025-04-23 16:46:23 +02:00
Guido D'Orsi
05ff90c3c4 feat: mark not found when the peer closes 2025-04-23 16:42:05 +02:00
Guido D'Orsi
07408970bd fix: return unavailable when loading from 0 peers 2025-04-23 16:40:31 +02:00
Guido D'Orsi
4ba3ea6b4e Merge remote-tracking branch 'origin/main' into refactor/simplify-covalue-state-anselm 2025-04-23 16:18:23 +02:00
Guido D'Orsi
c30fb098fe Merge remote-tracking branch 'origin/main' into refactor/simplify-covalue-state-anselm 2025-04-23 16:15:13 +02:00
Guido D'Orsi
a703bc3102 Merge pull request #1982 from garden-co/refactor/remove-fs-storage
Remove half-baked filesystem storage
2025-04-23 15:48:23 +02:00
Anselm
18dc96c7b1 Merge branch 'main' into refactor/simplify-covalue-state-anselm 2025-04-23 14:42:09 +01:00
Anselm
e9e7f45e02 Remove filesystem storage 2025-04-23 14:34:48 +01:00
Anselm
49fb6311ad Fix errors in jazz-tools 2025-04-23 12:37:29 +01:00
Anselm
cdc5cbd6d6 Don't retry coValue if it's available 2025-04-17 16:00:15 +01:00
Anselm
f55097c480 Fix broken test 2025-04-17 15:41:01 +01:00
Anselm
e9695fa2eb Interpret closed peers as "unavailable", clear up other edge cases 2025-04-17 15:33:15 +01:00
Anselm
0c11110567 Go back to string-y highLevelState and implement counters 2025-04-17 14:44:15 +01:00
Anselm
f2db858221 Distinguish between "available" and "received from peer" 2025-04-17 14:26:40 +01:00
Anselm
a362cbba51 Fix most retry-related tests 2025-04-17 13:03:55 +01:00
Anselm
39c2586d3b Wait in between peers, improve message test printing 2025-04-17 12:15:59 +01:00
Anselm
e5eed7bd35 Merge branch 'fix/peer-reconciliation' into refactor/simplify-covalue-state 2025-04-16 16:17:01 +01:00
Anselm
39ae497153 Use covalue state getters 2025-04-16 16:05:18 +01:00
Anselm
e0b5df7f9e Merge branch 'simplify-known-peer-state-updates' into refactor/simplify-covalue-state 2025-04-16 12:12:58 +01:00
Anselm
54b2907f08 WIP 2025-04-16 12:06:23 +01:00
Anselm
f3e0b1ed74 Remove dispatch pattern 2025-04-15 15:42:11 +01:00
152 changed files with 2655 additions and 2283 deletions

View File

@@ -1,5 +1,39 @@
# chat-rn-expo-clerk
## 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
- Updated dependencies [bd94012]
- jazz-expo@0.13.14
- jazz-tools@0.13.14
- jazz-react-native-media-images@0.13.14
## 1.0.105
### Patch Changes
- jazz-expo@0.13.13
- jazz-tools@0.13.13
- jazz-react-native-media-images@0.13.13
## 1.0.104
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-expo@0.13.12
- jazz-react-native-media-images@0.13.12
## 1.0.103
### Patch Changes

View File

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

View File

@@ -1,5 +1,35 @@
# chat-rn-expo
## 1.0.94
### Patch Changes
- jazz-expo@0.13.15
- jazz-tools@0.13.15
## 1.0.93
### Patch Changes
- Updated dependencies [bd94012]
- jazz-expo@0.13.14
- jazz-tools@0.13.14
## 1.0.92
### Patch Changes
- jazz-expo@0.13.13
- jazz-tools@0.13.13
## 1.0.91
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-expo@0.13.12
## 1.0.90
### Patch Changes

View File

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

View File

@@ -1,5 +1,46 @@
# chat-rn
## 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
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
- cojson-transport-ws@0.13.14
- jazz-react-native@0.13.14
- jazz-tools@0.13.14
## 1.0.100
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
- cojson-transport-ws@0.13.13
- jazz-react-native@0.13.13
- jazz-tools@0.13.13
## 1.0.99
### Patch Changes
- Updated dependencies [4547525]
- Updated dependencies [65719f2]
- jazz-tools@0.13.12
- cojson@0.13.12
- jazz-react-native@0.13.12
- cojson-transport-ws@0.13.12
## 1.0.98
### Patch Changes

View File

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

View File

@@ -1,5 +1,39 @@
# chat-vue
## 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
- jazz-browser@0.13.14
- jazz-tools@0.13.14
- jazz-vue@0.13.14
## 0.0.84
### Patch Changes
- jazz-browser@0.13.13
- jazz-tools@0.13.13
- jazz-vue@0.13.13
## 0.0.83
### Patch Changes
- Updated dependencies [4547525]
- Updated dependencies [29e05c4]
- jazz-tools@0.13.12
- jazz-browser@0.13.12
- jazz-vue@0.13.12
## 0.0.82
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# jazz-example-chat
## 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
- jazz-inspector@0.13.14
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.182
### Patch Changes
- jazz-inspector@0.13.13
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.181
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-inspector@0.13.12
- jazz-react@0.13.12
## 0.0.180
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# minimal-auth-clerk
## 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
- jazz-react@0.13.14
- jazz-react-auth-clerk@0.13.14
- jazz-tools@0.13.14
## 0.0.81
### Patch Changes
- jazz-react@0.13.13
- jazz-react-auth-clerk@0.13.13
- jazz-tools@0.13.13
## 0.0.80
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
- jazz-react-auth-clerk@0.13.12
## 0.0.79
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# file-share-svelte
## 0.0.66
### Patch Changes
- jazz-svelte@0.13.15
- jazz-tools@0.13.15
## 0.0.65
### Patch Changes
- jazz-svelte@0.13.14
- jazz-tools@0.13.14
## 0.0.64
### Patch Changes
- jazz-svelte@0.13.13
- jazz-tools@0.13.13
## 0.0.63
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-svelte@0.13.12
## 0.0.62
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# jazz-tailwind-demo-auth-starter
## 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
- jazz-inspector@0.13.14
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.21
### Patch Changes
- jazz-inspector@0.13.13
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.20
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-inspector@0.13.12
- jazz-react@0.13.12
## 0.0.19
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# form
## 0.1.24
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.1.23
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.1.22
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.1.21
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.1.20
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# image-upload
## 0.0.80
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.79
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.78
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.77
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.76
### Patch Changes

View File

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

View File

@@ -1,5 +1,41 @@
# jazz-example-inspector
## 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
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
- cojson-transport-ws@0.13.14
- jazz-inspector@0.13.14
## 0.0.132
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
- cojson-transport-ws@0.13.13
- jazz-inspector@0.13.13
## 0.0.131
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
- jazz-inspector@0.13.12
- cojson-transport-ws@0.13.12
## 0.0.130
### Patch Changes

View File

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

View File

@@ -45,7 +45,7 @@ export const Route = createFileRoute("/_authenticated/game/$gameId")({
});
function RouteComponent() {
const { gameId, me, loaderGame } = Route.useLoaderData();
const { gameId, loaderGame } = Route.useLoaderData();
const isPlayer1 = loaderGame.player1?.account?.isMe;
const player = isPlayer1 ? "player1" : "player2";

View File

@@ -1,4 +1,4 @@
import { Account, CoMap, SchemaUnion, co } from "jazz-tools";
import { Account, CoMap, co } from "jazz-tools";
export class Game extends CoMap {
player1 = co.ref(Player);

View File

@@ -1,5 +1,34 @@
# multi-cursors
## 0.0.76
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.75
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.74
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.73
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.72
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# multiauth
## 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
- jazz-react@0.13.14
- jazz-react-auth-clerk@0.13.14
- jazz-tools@0.13.14
## 0.0.22
### Patch Changes
- jazz-react@0.13.13
- jazz-react-auth-clerk@0.13.13
- jazz-tools@0.13.13
## 0.0.21
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
- jazz-react-auth-clerk@0.13.12
## 0.0.20
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# jazz-example-musicplayer
## 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
- jazz-inspector@0.13.14
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.103
### Patch Changes
- jazz-inspector@0.13.13
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.102
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-inspector@0.13.12
- jazz-react@0.13.12
## 0.0.101
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# organization
## 0.0.76
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.75
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.74
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.73
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.72
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# passkey-svelte
## 0.0.70
### Patch Changes
- jazz-svelte@0.13.15
- jazz-tools@0.13.15
## 0.0.69
### Patch Changes
- jazz-svelte@0.13.14
- jazz-tools@0.13.14
## 0.0.68
### Patch Changes
- jazz-svelte@0.13.13
- jazz-tools@0.13.13
## 0.0.67
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-svelte@0.13.12
## 0.0.66
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# minimal-auth-passkey
## 0.0.81
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.80
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.79
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.78
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.77
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# passphrase
## 0.0.78
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.77
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.76
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.75
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.74
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# jazz-password-manager
## 0.0.102
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.101
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.100
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.99
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.98
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# jazz-example-pets
## 0.0.200
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.199
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.198
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.197
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.196
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# reactions
## 0.0.80
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.79
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.78
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.77
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.76
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# richtext
## 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
- jazz-react@0.13.14
- jazz-tools@0.13.14
- jazz-richtext-prosemirror@0.1.3
## 0.0.68
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
- jazz-richtext-prosemirror@0.1.2
## 0.0.67
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
- jazz-richtext-prosemirror@0.1.1
## 0.0.66
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "richtext",
"private": true,
"version": "0.0.66",
"version": "0.0.70",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,39 @@
# todo-vue
## 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
- jazz-browser@0.13.14
- jazz-tools@0.13.14
- jazz-vue@0.13.14
## 0.0.82
### Patch Changes
- jazz-browser@0.13.13
- jazz-tools@0.13.13
- jazz-vue@0.13.13
## 0.0.81
### Patch Changes
- Updated dependencies [4547525]
- Updated dependencies [29e05c4]
- jazz-tools@0.13.12
- jazz-browser@0.13.12
- jazz-vue@0.13.12
## 0.0.80
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# jazz-example-todo
## 0.0.199
### Patch Changes
- jazz-react@0.13.15
- jazz-tools@0.13.15
## 0.0.198
### Patch Changes
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.197
### Patch Changes
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.196
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-react@0.13.12
## 0.0.195
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# version-history
## 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
- jazz-inspector@0.13.14
- jazz-react@0.13.14
- jazz-tools@0.13.14
## 0.0.76
### Patch Changes
- jazz-inspector@0.13.13
- jazz-react@0.13.13
- jazz-tools@0.13.13
## 0.0.75
### Patch Changes
- Updated dependencies [4547525]
- jazz-tools@0.13.12
- jazz-inspector@0.13.12
- jazz-react@0.13.12
## 0.0.74
### Patch Changes

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
import { clsx } from "clsx";
import { useEffect, useId, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Icon } from "../atoms/Icon";
// TODO: add tabs feature, and remove CodeExampleTabs
@@ -84,9 +84,25 @@ export function CodeGroup({
}) {
const textRef = useRef<HTMLPreElement | null>(null);
const [code, setCode] = useState<string>();
const filterText = (node: Node): string => {
if (
node instanceof Element &&
(node.classList.contains("twoslash-popup-container") ||
node.classList.contains("twoslash-completion-cursor"))
) {
return "";
}
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent ?? "";
}
return Array.from(node.childNodes).map(filterText).join("");
};
useEffect(() => {
if (textRef.current) {
setCode(textRef.current.innerText);
setCode(filterText(textRef.current));
}
}, [children]);

View File

@@ -0,0 +1,10 @@
"use client";
import { frameworkNames } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
export function Framework() {
const framework = useFramework();
return <>{frameworkNames[framework].label}</>;
}

View File

@@ -1,6 +1,6 @@
"use client";
import { Framework } from "@/content/framework";
import { Framework, frameworkNames } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import { Icon } from "@garden-co/design-system/src/components/atoms/Icon";
@@ -13,39 +13,6 @@ import {
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
const frameworks: Record<
Framework,
{
label: string;
experimental: boolean;
}
> = {
[Framework.React]: {
label: "React",
experimental: false,
},
[Framework.ReactNative]: {
label: "React Native",
experimental: false,
},
[Framework.ReactNativeExpo]: {
label: "React Native (Expo)",
experimental: false,
},
[Framework.Vanilla]: {
label: "VanillaJS",
experimental: false,
},
[Framework.Svelte]: {
label: "Svelte",
experimental: true,
},
[Framework.Vue]: {
label: "Vue",
experimental: true,
},
};
export function FrameworkSelect() {
const router = useRouter();
const defaultFramework = useFramework();
@@ -66,11 +33,11 @@ export function FrameworkSelect() {
as={Button}
variant="secondary"
>
{frameworks[selectedFramework].label}
{frameworkNames[selectedFramework].label}
<Icon name="chevronDown" size="sm" className="text-muted" />
</DropdownButton>
<DropdownMenu className="w-[--button-width] z-50" anchor="bottom start">
{Object.entries(frameworks).map(([key, framework]) => (
{Object.entries(frameworkNames).map(([key, framework]) => (
<DropdownItem
className="items-baseline"
key={key}

View File

@@ -12,6 +12,7 @@ import { CodeGroup as CodeGroupClient } from "@garden-co/design-system/src/compo
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";
import { FileDownloadLink as FileDownloadLinkClient } from "./FileDownloadLink";
import { ComingSoon as ComingSoonClient } from "./docs/ComingSoon";
import { Framework as FrameworkClient } from "./docs/Framework";
import { IssueTrackerPreview as IssueTrackerPreviewClient } from "./docs/IssueTrackerPreview";
export function CodeExampleTabs(props: CodeExampleTabsProps) {
@@ -46,3 +47,7 @@ export function FileDownloadLink(
) {
return <FileDownloadLinkClient {...props} />;
}
export function Framework() {
return <FrameworkClient />;
}

View File

@@ -70,7 +70,7 @@ If you are not working within a monorepo, create a new file `metro.config.js` in
// @noErrors: 2304
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(projectRoot);
const config = getDefaultConfig(__dirname);
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];

View File

@@ -33,7 +33,34 @@ const activityFeed = ActivityFeed.create([]);
```
</CodeGroup>
Like other CoValues, you can specify [ownership](/docs/using-covalues/ownership) when creating CoFeeds.
### Ownership
Like other CoValues, you can specify ownership when creating CoFeeds.
<CodeGroup>
```ts twoslash
import { Group, co, CoMap, CoFeed } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
class Activity extends CoMap {
timestamp = co.Date;
action = co.literal("watering", "planting", "harvesting", "maintenance");
notes = co.optional.string;
}
class ActivityFeed extends CoFeed.Of(co.ref(Activity)) {}
// ---cut---
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamFeed = ActivityFeed.create([], { owner: teamGroup });
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoFeeds.
## Reading from CoFeeds

View File

@@ -33,7 +33,32 @@ const tasks = ListOfTasks.create([
```
</CodeGroup>
Like other CoValues, you can specify [ownership](/docs/using-covalues/ownership) when creating CoLists.
### Ownership
Like other CoValues, you can specify ownership when creating CoLists.
<CodeGroup>
```ts twoslash
import { Group, co, CoMap, CoList } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
class Task extends CoMap {
title = co.string;
status = co.string;
}
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
// ---cut---
// Create with shared ownership
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
const teamList = ListOfTasks.create([], { owner: teamGroup });
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoLists.
## Reading from CoLists

View File

@@ -52,7 +52,24 @@ const inventory = Inventory.create({
When creating CoMaps, you can specify ownership to control access:
<CodeGroup>
```ts
```ts twoslash
import { Group, co, CoMap } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const memberAccount = await createJazzTestAccount();
class Member extends CoMap {
name = co.string;
}
class Project extends CoMap {
name = co.string;
startDate = co.Date;
status = co.literal("planning", "active", "completed");
coordinator = co.optional.ref(Member);
}
// ---cut---
// Create with default owner (current user)
const privateProject = Project.create({
name: "My Herb Garden",
@@ -75,6 +92,8 @@ const communityProject = Project.create(
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to CoMaps.
## Reading from CoMaps
CoMaps can be accessed using familiar JavaScript object notation:

View File

@@ -73,6 +73,29 @@ const fileStream = FileStream.create();
```
</CodeGroup>
### Ownership
Like other CoValues, you can specify ownership when creating FileStreams.
<CodeGroup>
```ts twoslash
import { Group, FileStream } from "jazz-tools";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
// ---cut---
// Create a team group
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
// Create a FileStream with shared ownership
const teamFileStream = FileStream.create({ owner: teamGroup });
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to FileStreams.
## Reading from FileStreams
`FileStream`s provide several ways to access their binary content, from raw chunks to convenient Blob objects.

View File

@@ -66,6 +66,31 @@ const image = await createImage(file, options);
```
</CodeGroup>
### Ownership
Like other CoValues, you can specify ownership when creating image definitions.
<CodeGroup>
```ts twoslash
import { Group } from "jazz-tools";
import { createImage } from "jazz-browser-media-images";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
const file = new File([], "test.jpg", { type: "image/jpeg" });
// ---cut---
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
// Create an image with shared ownership
const teamImage = await createImage(file, { owner: teamGroup });
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to images.
## Creating ImageDefinitions
Create an `ImageDefinition` by specifying the original dimensions and an optional placeholder:

View File

@@ -67,6 +67,31 @@ const image = await createImage(file, options);
```
</CodeGroup>
### Ownership
Like other CoValues, you can specify ownership when creating image definitions.
<CodeGroup>
```ts twoslash
import { Group } from "jazz-tools";
import { createImage } from "jazz-browser-media-images";
import { createJazzTestAccount } from 'jazz-tools/testing';
const me = await createJazzTestAccount();
const colleagueAccount = await createJazzTestAccount();
const file = new File([], "test.jpg", { type: "image/jpeg" });
// ---cut---
const teamGroup = Group.create();
teamGroup.addMember(colleagueAccount, "writer");
// Create an image with shared ownership
const teamImage = await createImage(file, { owner: teamGroup });
```
</CodeGroup>
See [Groups as permission scopes](/docs/groups/intro) for more information on how to use groups to control access to images.
## Displaying Images with `ProgressiveImg`
For a complete progressive loading experience, use the `ProgressiveImg` component:

View File

@@ -8,6 +8,38 @@ export enum Framework {
}
export const frameworks = Object.values(Framework);
export const frameworkNames: Record<
Framework,
{
label: string;
experimental: boolean;
}
> = {
[Framework.React]: {
label: "React",
experimental: false,
},
[Framework.ReactNative]: {
label: "React Native",
experimental: false,
},
[Framework.ReactNativeExpo]: {
label: "React Native (Expo)",
experimental: false,
},
[Framework.Vanilla]: {
label: "VanillaJS",
experimental: false,
},
[Framework.Svelte]: {
label: "Svelte",
experimental: true,
},
[Framework.Vue]: {
label: "Vue",
experimental: true,
},
};
export const DEFAULT_FRAMEWORK = Framework.React;

View File

@@ -42,7 +42,7 @@ export async function getDocMetadata(framework: string, slug?: string[]) {
function DocProse({ children }: { children: React.ReactNode }) {
return (
<Prose className="overflow-x-visible lg:flex-1 pb-8 pt-[calc(61px+2rem)] md:pt-8 md:max-w-3xl mx-auto">
<Prose className="overflow-x-hidden lg:overflow-x-visible lg:flex-1 pb-8 pt-[calc(61px+2rem)] md:pt-8 md:max-w-3xl mx-auto">
{children}
</Prose>
);

View File

@@ -1,5 +1,37 @@
# cojson-storage-indexeddb
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-storage@0.13.15
## 0.13.14
### Patch Changes
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
- cojson-storage@0.13.14
## 0.13.13
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
- cojson-storage@0.13.13
## 0.13.12
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
- cojson-storage@0.13.12
## 0.13.11
### Patch Changes

View File

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

View File

@@ -1,5 +1,37 @@
# cojson-storage-sqlite
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
- cojson-storage@0.13.15
## 0.13.14
### Patch Changes
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
- cojson-storage@0.13.14
## 0.13.13
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
- cojson-storage@0.13.13
## 0.13.12
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
- cojson-storage@0.13.12
## 0.13.11
### Patch Changes

View File

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

View File

@@ -118,9 +118,9 @@ test("should sync and load data from storage", async () => {
"client -> LOAD Map sessions: empty",
"storage -> KNOWN Group sessions: header/3",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"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",
]
`);
@@ -205,12 +205,12 @@ test("should load dependencies correctly (group inheritance)", async () => {
"client -> LOAD Map sessions: empty",
"storage -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> KNOWN Group sessions: header/5",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"client -> KNOWN ParentGroup sessions: header/4",
"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",
]
`);
@@ -312,9 +312,9 @@ test("should recover from data loss", async () => {
"client -> LOAD Map sessions: empty",
"storage -> KNOWN Group sessions: header/3",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"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,33 @@
# cojson-storage
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
## 0.13.14
### Patch Changes
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
## 0.13.13
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
## 0.13.12
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
## 0.13.11
### Patch Changes

View File

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

View File

@@ -1,5 +1,33 @@
# cojson-transport-nodejs-ws
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
## 0.13.14
### Patch Changes
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
## 0.13.13
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
## 0.13.12
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
## 0.13.11
### Patch Changes

View File

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

View File

@@ -1,5 +1,29 @@
# cojson
## 0.13.15
### Patch Changes
- c712ef2: Revert the RawCoList incremental processing
## 0.13.14
### Patch Changes
- 5c2c7d4: Make the incoming messages handling in the sync manager syncronous
## 0.13.13
### Patch Changes
- ec9cb40: Remove .every() call on iterator to fix compat issues with React Native
## 0.13.12
### Patch Changes
- 65719f2: Simplified CoValue loading state management
## 0.13.11
### Patch Changes

View File

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

View File

@@ -1,6 +1,7 @@
import { CoValueCore } from "./coValueCore.js";
import { CoValueState } from "./coValueState.js";
import { RawCoID } from "./ids.js";
import { PeerID } from "./sync.js";
export class CoValuesStore {
coValues = new Map<RawCoID, CoValueState>();
@@ -9,19 +10,21 @@ export class CoValuesStore {
let entry = this.coValues.get(id);
if (!entry) {
entry = CoValueState.Unknown(id);
entry = new CoValueState(id);
this.coValues.set(id, entry);
}
return entry;
}
setAsAvailable(id: RawCoID, coValue: CoValueCore) {
markAsAvailable(id: RawCoID, coValue: CoValueCore, fromPeerId: PeerID) {
const entry = this.get(id);
entry.dispatch({
type: "available",
coValue,
});
entry.markAvailable(coValue, fromPeerId);
}
internalMarkMagicallyAvailable(id: RawCoID, coValue: CoValueCore) {
const entry = this.get(id);
entry.internalMarkMagicallyAvailable(coValue);
}
getEntries() {

View File

@@ -1,9 +1,5 @@
import { PeerKnownStates, ReadonlyPeerKnownStates } from "./PeerKnownStates.js";
import {
PriorityBasedMessageQueue,
QueueEntry,
} from "./PriorityBasedMessageQueue.js";
import { TryAddTransactionsError } from "./coValueCore.js";
import { PriorityBasedMessageQueue } from "./PriorityBasedMessageQueue.js";
import { RawCoID, SessionID } from "./ids.js";
import { logger } from "./logger.js";
import { CO_VALUE_PRIORITY } from "./priority.js";
@@ -12,9 +8,6 @@ import { CoValueKnownState, Peer, SyncMessage } from "./sync.js";
export class PeerState {
private queue: PriorityBasedMessageQueue;
incomingMessagesProcessingPromise: Promise<void> | undefined;
nextPeer: Peer | undefined;
constructor(
private peer: Peer,
knownStates: ReadonlyPeerKnownStates | undefined,
@@ -122,8 +115,6 @@ export class PeerState {
}
}
readonly erroredCoValues: Map<RawCoID, TryAddTransactionsError> = new Map();
get id() {
return this.peer.id;
}
@@ -158,15 +149,26 @@ export class PeerState {
this.processing = true;
let entry: QueueEntry | undefined;
while ((entry = this.queue.pull())) {
let msg: SyncMessage | undefined;
while ((msg = this.queue.pull())) {
if (this.closed) {
break;
}
// Awaiting the push to send one message at a time
// This way when the peer is "under pressure" we can enqueue all
// the coming messages and organize them by priority
await this.peer.outgoing
.push(entry.msg)
.then(entry.resolve)
.catch(entry.reject);
try {
await this.peer.outgoing.push(msg);
} catch (e) {
logger.error("Error sending message", {
err: e,
action: msg.action,
id: msg.id,
peerId: this.id,
peerRole: this.role,
});
}
}
this.processing = false;
@@ -174,14 +176,16 @@ export class PeerState {
pushOutgoingMessage(msg: SyncMessage) {
if (this.closed) {
return Promise.resolve();
return;
}
const promise = this.queue.push(msg);
this.queue.push(msg);
void this.processQueue();
}
return promise;
isProcessing() {
return this.processing;
}
get incoming() {
@@ -194,14 +198,6 @@ export class PeerState {
return this.peer.incoming;
}
private closeQueue() {
let entry: QueueEntry | undefined;
while ((entry = this.queue.pull())) {
// Using resolve here to avoid unnecessary noise in the logs
entry.resolve();
}
}
closeListeners = new Set<() => void>();
addCloseListener(listener: () => void) {
@@ -230,40 +226,38 @@ export class PeerState {
peerId: this.id,
peerRole: this.role,
});
this.closeQueue();
this.peer.outgoing.close();
this.closed = true;
this.emitClose();
}
async processIncomingMessages(callback: (msg: SyncMessage) => Promise<void>) {
async processIncomingMessages(callback: (msg: SyncMessage) => void) {
if (this.closed) {
throw new Error("Peer is closed");
}
if (this.incomingMessagesProcessingPromise) {
throw new Error("Incoming messages processing already in progress");
}
const processIncomingMessages = async () => {
for await (const msg of this.incoming) {
if (msg === "Disconnected") {
break;
if (this.closed) {
return;
}
if (msg === "Disconnected") {
return;
}
if (msg === "PingTimeout") {
logger.error("Ping timeout from peer", {
peerId: this.id,
peerRole: this.role,
});
break;
return;
}
await callback(msg);
callback(msg);
}
};
this.incomingMessagesProcessingPromise = processIncomingMessages();
return this.incomingMessagesProcessingPromise;
return processIncomingMessages();
}
}

View File

@@ -2,29 +2,6 @@ import { Counter, ValueType, metrics } from "@opentelemetry/api";
import { CO_VALUE_PRIORITY, type CoValuePriority } from "./priority.js";
import type { SyncMessage } from "./sync.js";
function promiseWithResolvers<R>() {
let resolve = (_: R) => {};
let reject = (_: unknown) => {};
const promise = new Promise<R>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return {
promise,
resolve,
reject,
};
}
export type QueueEntry = {
msg: SyncMessage;
promise: Promise<void>;
resolve: () => void;
reject: (_: unknown) => void;
};
/**
* Since we have a fixed range of priority values (0-7) we can create a fixed array of queues.
*/
@@ -34,7 +11,7 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
? A
: Tuple<T, N, [...A, T]>;
type QueueTuple = Tuple<LinkedList<QueueEntry>, 3>;
type QueueTuple = Tuple<LinkedList<SyncMessage>, 3>;
type LinkedListNode<T> = {
value: T;
@@ -164,14 +141,9 @@ export class PriorityBasedMessageQueue {
}
public push(msg: SyncMessage) {
const { promise, resolve, reject } = promiseWithResolvers<void>();
const entry: QueueEntry = { msg, promise, resolve, reject };
const priority = "priority" in msg ? msg.priority : this.defaultPriority;
this.getQueue(priority).push(entry);
return promise;
this.getQueue(priority).push(msg);
}
public pull() {

View File

@@ -116,11 +116,11 @@ export class SyncStateManager {
const entry = this.syncManager.local.coValuesStore.get(id);
if (entry.state.type !== "available") {
if (!entry.isAvailable()) {
return undefined;
}
const coValue = entry.state.coValue;
const coValue = entry.core;
const coValueSessions = coValue.knownState().sessions;
return {

View File

@@ -136,8 +136,8 @@ export class CoValueCore {
const groupId = header.ruleset.group;
const entry = this.node.coValuesStore.get(groupId);
if (entry.state.type === "available") {
this.groupInvalidationSubscription = entry.state.coValue.subscribe(
if (entry.isAvailable()) {
this.groupInvalidationSubscription = entry.core.subscribe(
(_groupUpdate) => {
this._cachedContent = undefined;
this.notifyUpdate("immediate");
@@ -488,7 +488,7 @@ export class CoValueCore {
if (success) {
this.node.syncManager.recordTransactionsSize([transaction], "local");
void this.node.syncManager.syncCoValue(this);
void this.node.syncManager.requestCoValueSync(this);
}
return success;

View File

@@ -1,119 +1,35 @@
import { ValueType } from "@opentelemetry/api";
import { UpDownCounter, metrics } from "@opentelemetry/api";
import { PeerState } from "./PeerState.js";
import { CoValueCore } from "./coValueCore.js";
import { CoValueCore, TryAddTransactionsError } from "./coValueCore.js";
import { RawCoID } from "./ids.js";
import { logger } from "./logger.js";
import { PeerID } from "./sync.js";
import { PeerID, emptyKnownState } from "./sync.js";
export const CO_VALUE_LOADING_CONFIG = {
MAX_RETRIES: 2,
TIMEOUT: 30_000,
};
export class CoValueUnknownState {
type = "unknown" as const;
}
export class CoValueLoadingState {
type = "loading" as const;
export class CoValueState {
private peers = new Map<
PeerID,
ReturnType<typeof createResolvablePromise<void>>
| { type: "unknown" | "pending" | "available" | "unavailable" }
| {
type: "errored";
error: TryAddTransactionsError;
}
>();
private resolveResult: (value: CoValueCore | "unavailable") => void;
result: Promise<CoValueCore | "unavailable">;
core: CoValueCore | null = null;
id: RawCoID;
constructor(peersIds: Iterable<PeerID>) {
this.peers = new Map();
for (const peerId of peersIds) {
this.peers.set(peerId, createResolvablePromise<void>());
}
const { resolve, promise } = createResolvablePromise<
CoValueCore | "unavailable"
>();
this.result = promise;
this.resolveResult = resolve;
}
markAsUnavailable(peerId: PeerID) {
const entry = this.peers.get(peerId);
if (entry) {
entry.resolve();
}
this.peers.delete(peerId);
// If none of the peers have the coValue, we resolve to unavailable
if (this.peers.size === 0) {
this.resolve("unavailable");
}
}
resolve(value: CoValueCore | "unavailable") {
this.resolveResult(value);
for (const entry of this.peers.values()) {
entry.resolve();
}
this.peers.clear();
}
// Wait for a specific peer to have a known state
waitForPeer(peerId: PeerID) {
const entry = this.peers.get(peerId);
if (!entry) {
return Promise.resolve();
}
return entry.promise;
}
}
export class CoValueAvailableState {
type = "available" as const;
constructor(public coValue: CoValueCore) {}
}
export class CoValueUnavailableState {
type = "unavailable" as const;
}
type CoValueStateAction =
| {
type: "load-requested";
peersIds: PeerID[];
}
| {
type: "not-found-in-peer";
peerId: PeerID;
}
| {
type: "available";
coValue: CoValueCore;
};
type CoValueStateType =
| CoValueUnknownState
| CoValueLoadingState
| CoValueAvailableState
| CoValueUnavailableState;
export class CoValueState {
promise?: Promise<CoValueCore | "unavailable">;
private resolve?: (value: CoValueCore | "unavailable") => void;
private listeners: Set<(state: CoValueState) => void> = new Set();
private counter: UpDownCounter;
constructor(
public id: RawCoID,
public state: CoValueStateType,
) {
constructor(id: RawCoID) {
this.id = id;
this.counter = metrics
.getMeter("cojson")
.createUpDownCounter("jazz.covalues.loaded", {
@@ -122,128 +38,162 @@ export class CoValueState {
valueType: ValueType.INT,
});
this.counter.add(1, {
state: this.state.type,
});
this.updateCounter(null);
}
static Unknown(id: RawCoID) {
return new CoValueState(id, new CoValueUnknownState());
get highLevelState() {
if (this.core) {
return "available";
} else if (this.peers.size === 0) {
return "unknown";
}
for (const peer of this.peers.values()) {
if (peer.type === "pending") {
return "loading";
} else if (peer.type === "unknown") {
return "unknown";
}
}
return "unavailable";
}
static Loading(id: RawCoID, peersIds: Iterable<PeerID>) {
return new CoValueState(id, new CoValueLoadingState(peersIds));
isErroredInPeer(peerId: PeerID) {
return this.peers.get(peerId)?.type === "errored";
}
static Available(coValue: CoValueCore) {
return new CoValueState(coValue.id, new CoValueAvailableState(coValue));
isAvailable(): this is { type: "available"; core: CoValueCore } {
return !!this.core;
}
static Unavailable(id: RawCoID) {
return new CoValueState(id, new CoValueUnavailableState());
addListener(listener: (state: CoValueState) => void) {
this.listeners.add(listener);
listener(this);
}
removeListener(listener: (state: CoValueState) => void) {
this.listeners.delete(listener);
}
private notifyListeners() {
for (const listener of this.listeners) {
listener(this);
}
}
async getCoValue() {
if (this.state.type === "available") {
return this.state.coValue;
}
if (this.state.type === "unavailable") {
if (this.highLevelState === "unavailable") {
return "unavailable";
}
// If we don't have a resolved state we return a new promise
// that will be resolved when the state will move to available or unavailable
if (!this.promise) {
const { promise, resolve } = createResolvablePromise<
CoValueCore | "unavailable"
>();
return new Promise<CoValueCore>((resolve) => {
const listener = (state: CoValueState) => {
if (state.core) {
resolve(state.core);
this.removeListener(listener);
}
};
this.promise = promise;
this.resolve = resolve;
}
return this.promise;
}
private moveToState(value: CoValueStateType) {
this.counter.add(-1, {
state: this.state.type,
this.addListener(listener);
});
this.state = value;
this.counter.add(1, {
state: this.state.type,
});
if (!this.resolve) {
return;
}
// If the state is available we resolve the promise
// and clear it to handle the possible transition from unavailable to available
if (value.type === "available") {
this.resolve(value.coValue);
this.clearPromise();
} else if (value.type === "unavailable") {
this.resolve("unavailable");
this.clearPromise();
}
}
private clearPromise() {
this.promise = undefined;
this.resolve = undefined;
}
async loadFromPeers(peers: PeerState[]) {
const state = this.state;
if (state.type === "loading" || state.type === "available") {
return;
}
if (peers.length === 0) {
this.moveToState(new CoValueUnavailableState());
return;
}
const doLoad = async (peersToLoadFrom: PeerState[]) => {
const peersWithoutErrors = getPeersWithoutErrors(
peersToLoadFrom,
this.id,
);
const loadAttempt = async (peersToLoadFrom: PeerState[]) => {
const peersToActuallyLoadFrom = [];
for (const peer of peersToLoadFrom) {
const currentState = this.peers.get(peer.id);
// If we are in the loading state we move to a new loading state
// to reset all the loading promises
if (
this.state.type === "loading" ||
this.state.type === "unknown" ||
this.state.type === "unavailable"
) {
this.moveToState(
new CoValueLoadingState(peersWithoutErrors.map((p) => p.id)),
);
if (currentState?.type === "available") {
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") {
this.markPending(peer.id);
peersToActuallyLoadFrom.push(peer);
}
}
// Assign the current state to a variable to not depend on the state changes
// that may happen while we wait for loadCoValueFromPeers to complete
const currentState = this.state;
for (const peer of peersToActuallyLoadFrom) {
if (peer.closed) {
this.markNotFoundInPeer(peer.id);
continue;
}
// If we entered successfully the loading state, we load the coValue from the peers
//
// We may not enter the loading state if the coValue has become available in between
// of the retries
if (currentState.type === "loading") {
await loadCoValueFromPeers(this, peersWithoutErrors);
peer.pushOutgoingMessage({
action: "load",
...(this.core ? this.core.knownState() : emptyKnownState(this.id)),
});
const result = await currentState.result;
return result !== "unavailable";
/**
* 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;
}
return currentState.type === "available";
};
await doLoad(peers);
await loadAttempt(peers);
if (this.isAvailable()) {
return;
}
// Retry loading from peers that have the retry flag enabled
const peersWithRetry = peers.filter((p) =>
@@ -251,129 +201,74 @@ export class CoValueState {
);
if (peersWithRetry.length > 0) {
const waitingForCoValue = new Promise<void>((resolve) => {
const listener = (state: CoValueState) => {
if (state.isAvailable()) {
resolve();
this.removeListener(listener);
}
};
this.addListener(listener);
});
// We want to exit early if the coValue becomes available in between the retries
await Promise.race([
this.getCoValue(),
waitingForCoValue,
runWithRetry(
() => doLoad(peersWithRetry),
() => loadAttempt(peersWithRetry),
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
),
]);
}
}
// If after the retries the coValue is still loading, we consider the load failed
if (this.state.type === "loading") {
this.moveToState(new CoValueUnavailableState());
private updateCounter(previousState: string | null) {
const newState = this.highLevelState;
if (previousState !== newState) {
if (previousState) {
this.counter.add(-1, { state: previousState });
}
this.counter.add(1, { state: newState });
}
}
dispatch(action: CoValueStateAction) {
const currentState = this.state;
switch (action.type) {
case "available":
if (currentState.type === "loading") {
currentState.resolve(action.coValue);
}
// It should be always possible to move to the available state
this.moveToState(new CoValueAvailableState(action.coValue));
break;
case "not-found-in-peer":
if (currentState.type === "loading") {
currentState.markAsUnavailable(action.peerId);
}
break;
}
markNotFoundInPeer(peerId: PeerID) {
const previousState = this.highLevelState;
this.peers.set(peerId, { type: "unavailable" });
this.updateCounter(previousState);
this.notifyListeners();
}
}
async function loadCoValueFromPeers(
coValueEntry: CoValueState,
peers: PeerState[],
) {
for (const peer of peers) {
if (peer.closed) {
coValueEntry.dispatch({
type: "not-found-in-peer",
peerId: peer.id,
});
continue;
}
// TODO: rename to "provided"
markAvailable(coValue: CoValueCore, fromPeerId: PeerID) {
const previousState = this.highLevelState;
this.core = coValue;
this.peers.set(fromPeerId, { type: "available" });
this.updateCounter(previousState);
this.notifyListeners();
}
if (coValueEntry.state.type === "available") {
/**
* We don't need to wait for the message to be delivered here.
*
* This way when the coValue becomes available because it's cached we don't wait for the server
* peer to consume the messages queue before moving forward.
*/
peer
.pushOutgoingMessage({
action: "load",
...coValueEntry.state.coValue.knownState(),
})
.catch((err) => {
logger.warn(`Failed to push load message to peer ${peer.id}`, {
err,
});
});
} else {
/**
* We only wait for the load state to be resolved.
*/
peer
.pushOutgoingMessage({
action: "load",
id: coValueEntry.id,
header: false,
sessions: {},
})
.catch((err) => {
logger.warn(`Failed to push load message to peer ${peer.id}`, {
err,
});
});
}
internalMarkMagicallyAvailable(coValue: CoValueCore) {
const previousState = this.highLevelState;
this.core = coValue;
this.updateCounter(previousState);
this.notifyListeners();
}
if (coValueEntry.state.type === "loading") {
const { promise, resolve } = createResolvablePromise<void>();
markErrored(peerId: PeerID, error: TryAddTransactionsError) {
const previousState = this.highLevelState;
this.peers.set(peerId, { type: "errored", error });
this.updateCounter(previousState);
this.notifyListeners();
}
/**
* 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 handleTimeoutOrClose = () => {
if (coValueEntry.state.type === "loading") {
logger.warn("Failed to load coValue from peer", {
coValueId: coValueEntry.id,
peerId: peer.id,
peerRole: peer.role,
});
coValueEntry.dispatch({
type: "not-found-in-peer",
peerId: peer.id,
});
resolve();
}
};
const timeout = setTimeout(handleTimeoutOrClose, timeoutDuration);
const closeListener = peer.addCloseListener(handleTimeoutOrClose);
await Promise.race([promise, coValueEntry.state.waitForPeer(peer.id)]);
clearTimeout(timeout);
closeListener();
}
private markPending(peerId: PeerID) {
const previousState = this.highLevelState;
this.peers.set(peerId, { type: "pending" });
this.updateCounter(previousState);
this.notifyListeners();
}
}
@@ -400,29 +295,6 @@ async function runWithRetry<T>(fn: () => Promise<T>, maxRetries: number) {
}
}
function createResolvablePromise<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
resolve = res;
});
return { promise, resolve };
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getPeersWithoutErrors(peers: PeerState[], coValueId: RawCoID) {
return peers.filter((p) => {
if (p.erroredCoValues.has(coValueId)) {
logger.warn(
`Skipping load on errored coValue ${coValueId} from peer ${p.id}`,
);
return false;
}
return true;
});
}

View File

@@ -2,7 +2,6 @@ import { CoID, RawCoValue } from "../coValue.js";
import { CoValueCore } from "../coValueCore.js";
import { AgentID, SessionID, TransactionID } from "../ids.js";
import { JsonObject, JsonValue } from "../jsonValue.js";
import { CoValueKnownState } from "../sync.js";
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
import { isCoValue } from "../typeUtils/isCoValue.js";
import { RawAccountID } from "./account.js";
@@ -82,8 +81,6 @@ export class RawCoListView<
madeAt: number;
opID: OpID;
}[];
/** @internal */
knownTransactions: CoValueKnownState["sessions"];
/** @internal */
constructor(core: CoValueCore) {
@@ -98,24 +95,12 @@ export class RawCoListView<
this.deletionsByInsertion = {};
this.afterStart = [];
this.beforeEnd = [];
this.knownTransactions = {};
this.processNewTransactions();
}
processNewTransactions() {
const newTransactions = this.core.getValidTransactions({
ignorePrivateTransactions: false,
knownTransactions: this.knownTransactions,
});
if (newTransactions.length === 0) {
return;
}
this._cachedEntries = undefined;
for (const { txID, changes, madeAt } of newTransactions) {
for (const {
txID,
changes,
madeAt,
} of this.core.getValidSortedTransactions()) {
for (const [changeIdx, changeUntyped] of changes.entries()) {
const change = changeUntyped as ListOpPayload<Item>;
@@ -207,11 +192,6 @@ export class RawCoListView<
);
}
}
this.knownTransactions[txID.sessionID] = Math.max(
this.knownTransactions[txID.sessionID] ?? 0,
txID.txIndex,
);
}
}
@@ -501,7 +481,7 @@ export class RawCoList<
this.core.makeTransaction(changes, privacy);
this.processNewTransactions();
this.rebuildFromCore();
}
/**
@@ -548,7 +528,7 @@ export class RawCoList<
privacy,
);
this.processNewTransactions();
this.rebuildFromCore();
}
/** Deletes the item at index `at`.
@@ -575,7 +555,7 @@ export class RawCoList<
privacy,
);
this.processNewTransactions();
this.rebuildFromCore();
}
replace(
@@ -603,6 +583,17 @@ export class RawCoList<
],
privacy,
);
this.processNewTransactions();
this.rebuildFromCore();
}
/** @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

@@ -187,6 +187,6 @@ export class RawCoPlainText<
}
this.core.makeTransaction(ops, privacy);
this.processNewTransactions();
this.rebuildFromCore();
}
}

View File

@@ -186,8 +186,8 @@ export class RawGroup<
const child = store.get(id);
if (
child.state.type === "unknown" ||
child.state.type === "unavailable"
child.highLevelState === "unknown" ||
child.highLevelState === "unavailable"
) {
child.loadFromPeers(peers).catch(() => {
logger.error(`Failed to load child group ${id}`);

View File

@@ -79,8 +79,6 @@ type Value = JsonValue | AnyRawCoValue;
import { CO_VALUE_LOADING_CONFIG } from "./coValueState.js";
import { logger } from "./logger.js";
import { getPriorityFromHeader } from "./priority.js";
import { FileSystem } from "./storage/FileSystem.js";
import { BlockFilename, LSMStorage, WalFilename } from "./storage/index.js";
/** @hidden */
export const cojsonInternals = {
@@ -143,7 +141,6 @@ export {
CryptoProvider,
SyncMessage,
isRawCoID,
LSMStorage,
emptyKnownState,
RawCoPlainText,
stringifyOpID,
@@ -154,9 +151,6 @@ export {
export type {
Value,
FileSystem,
BlockFilename,
WalFilename,
IncomingSyncStream,
OutgoingSyncQueue,
DisconnectedError,

View File

@@ -126,7 +126,7 @@ export class LocalNode {
);
nodeWithAccount.account = controlledAccount;
nodeWithAccount.coValuesStore.setAsAvailable(
nodeWithAccount.coValuesStore.internalMarkMagicallyAvailable(
controlledAccount.id,
controlledAccount.core,
);
@@ -139,9 +139,9 @@ export class LocalNode {
// we shouldn't need this, but it fixes account data not syncing for new accounts
function syncAllCoValuesAfterCreateAccount() {
for (const coValueEntry of nodeWithAccount.coValuesStore.getValues()) {
if (coValueEntry.state.type === "available") {
void nodeWithAccount.syncManager.syncCoValue(
coValueEntry.state.coValue,
if (coValueEntry.isAvailable()) {
void nodeWithAccount.syncManager.requestCoValueSync(
coValueEntry.core,
);
}
}
@@ -208,7 +208,10 @@ export class LocalNode {
node.syncManager.local = node;
controlledAccount.core.node = node;
node.coValuesStore.setAsAvailable(accountID, controlledAccount.core);
node.coValuesStore.internalMarkMagicallyAvailable(
accountID,
controlledAccount.core,
);
controlledAccount.core._cachedContent = undefined;
const profileID = account.get("profile");
@@ -245,9 +248,9 @@ export class LocalNode {
}
const coValue = new CoValueCore(header, this);
this.coValuesStore.setAsAvailable(coValue.id, coValue);
this.coValuesStore.internalMarkMagicallyAvailable(coValue.id, coValue);
void this.syncManager.syncCoValue(coValue);
void this.syncManager.requestCoValueSync(coValue);
return coValue;
}
@@ -265,10 +268,17 @@ export class LocalNode {
const entry = this.coValuesStore.get(id);
if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
if (
entry.highLevelState === "unknown" ||
entry.highLevelState === "unavailable"
) {
const peers =
this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
if (peers.length === 0) {
return "unavailable";
}
await entry.loadFromPeers(peers).catch((e) => {
logger.error("Error loading from peers", {
id,
@@ -309,8 +319,8 @@ export class LocalNode {
getLoaded<T extends RawCoValue>(id: CoID<T>): T | undefined {
const entry = this.coValuesStore.get(id);
if (entry.state.type === "available") {
return entry.state.coValue.getCurrentContent() as T;
if (entry.isAvailable()) {
return entry.core.getCurrentContent() as T;
}
return undefined;
@@ -439,12 +449,12 @@ export class LocalNode {
expectCoValueLoaded(id: RawCoID, expectation?: string): CoValueCore {
const entry = this.coValuesStore.get(id);
if (entry.state.type !== "available") {
if (!entry.isAvailable()) {
throw new Error(
`${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded. Current state: ${entry.state.type}`,
`${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded. Current state: ${JSON.stringify(entry)}`,
);
}
return entry.state.coValue;
return entry.core;
}
/** @internal */
@@ -638,15 +648,13 @@ export class LocalNode {
while (coValuesToCopy.length > 0) {
const [coValueID, entry] = coValuesToCopy[coValuesToCopy.length - 1]!;
if (entry.state.type !== "available") {
if (!entry.isAvailable()) {
coValuesToCopy.pop();
continue;
} else {
const allDepsCopied = entry.state.coValue
const allDepsCopied = entry.core
.getDependedOnCoValues()
.every(
(dep) => newNode.coValuesStore.get(dep).state.type === "available",
);
.every((dep) => newNode.coValuesStore.get(dep).isAvailable());
if (!allDepsCopied) {
// move to end of queue
@@ -655,12 +663,15 @@ export class LocalNode {
}
const newCoValue = new CoValueCore(
entry.state.coValue.header,
entry.core.header,
newNode,
new Map(entry.state.coValue.sessionLogs),
new Map(entry.core.sessionLogs),
);
newNode.coValuesStore.setAsAvailable(coValueID, newCoValue);
newNode.coValuesStore.internalMarkMagicallyAvailable(
coValueID,
newCoValue,
);
coValuesToCopy.pop();
}

View File

@@ -1,113 +0,0 @@
import { CryptoProvider, StreamingHash } from "../crypto/crypto.js";
import { RawCoID } from "../ids.js";
import { CoValueChunk } from "./index.js";
export type BlockFilename = `L${number}-${string}-${string}-H${number}.jsonl`;
export type BlockHeader = { id: RawCoID; start: number; length: number }[];
export type WalEntry = { id: RawCoID } & CoValueChunk;
export type WalFilename = `wal-${number}.jsonl`;
export interface FileSystem<WriteHandle, ReadHandle> {
crypto: CryptoProvider;
createFile(filename: string): Promise<WriteHandle>;
append(handle: WriteHandle, data: Uint8Array): Promise<void>;
close(handle: ReadHandle | WriteHandle): Promise<void>;
closeAndRename(handle: WriteHandle, filename: BlockFilename): Promise<void>;
openToRead(filename: string): Promise<{ handle: ReadHandle; size: number }>;
read(handle: ReadHandle, offset: number, length: number): Promise<Uint8Array>;
listFiles(): Promise<string[]>;
removeFile(filename: BlockFilename | WalFilename): Promise<void>;
}
export const textEncoder = new TextEncoder();
export const textDecoder = new TextDecoder();
export async function readChunk<RH, FS extends FileSystem<unknown, RH>>(
handle: RH,
header: { start: number; length: number },
fs: FS,
): Promise<CoValueChunk> {
const chunkBytes = await fs.read(handle, header.start, header.length);
const chunk = JSON.parse(textDecoder.decode(chunkBytes));
return chunk;
}
export async function readHeader<RH, FS extends FileSystem<unknown, RH>>(
filename: string,
handle: RH,
size: number,
fs: FS,
): Promise<BlockHeader> {
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
const headerBytes = await fs.read(handle, size - headerLength, headerLength);
const header = JSON.parse(textDecoder.decode(headerBytes));
return header;
}
export async function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
chunks: Map<RawCoID, CoValueChunk>,
level: number,
blockNumber: number,
fs: FS,
): Promise<BlockFilename> {
if (chunks.size === 0) {
throw new Error("No chunks to write");
}
const blockHeader: BlockHeader = [];
let offset = 0;
const file = await fs.createFile(
"wipBlock" + Math.random().toString(36).substring(7) + ".tmp.jsonl",
);
const hash = new StreamingHash(fs.crypto);
const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) =>
id1.localeCompare(id2),
);
for (const [id, chunk] of chunksSortedById) {
const encodedBytes = hash.update(chunk);
const encodedBytesWithNewline = new Uint8Array(encodedBytes.length + 1);
encodedBytesWithNewline.set(encodedBytes);
encodedBytesWithNewline[encodedBytes.length] = 10;
await fs.append(file, encodedBytesWithNewline);
const length = encodedBytesWithNewline.length;
blockHeader.push({ id, start: offset, length });
offset += length;
}
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
await fs.append(file, headerBytes);
const filename: BlockFilename = `L${level}-${(blockNumber + "").padStart(
3,
"0",
)}-${hash.digest().replace("hash_", "").slice(0, 15)}-H${
headerBytes.length
}.jsonl`;
await fs.closeAndRename(file, filename);
return filename;
}
export async function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
handle: WH,
fs: FS,
id: RawCoID,
chunk: CoValueChunk,
) {
const walEntry: WalEntry = {
id,
...chunk,
};
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
return fs.append(handle, bytes);
}

View File

@@ -1,137 +0,0 @@
import { MAX_RECOMMENDED_TX_SIZE } from "../coValueCore.js";
import { RawCoID, SessionID } from "../ids.js";
import { getPriorityFromHeader } from "../priority.js";
import { CoValueKnownState, NewContentMessage } from "../sync.js";
import { CoValueChunk } from "./index.js";
export function contentSinceChunk(
id: RawCoID,
chunk: CoValueChunk,
known?: CoValueKnownState,
): NewContentMessage[] {
const newContentPieces: NewContentMessage[] = [];
newContentPieces.push({
id: id,
action: "content",
header: known?.header ? undefined : chunk.header,
new: {},
priority: getPriorityFromHeader(chunk.header),
});
for (const [sessionID, sessionsEntry] of Object.entries(
chunk.sessionEntries,
)) {
for (const entry of sessionsEntry) {
const knownStart = known?.sessions[sessionID as SessionID] || 0;
if (entry.after + entry.transactions.length <= knownStart) {
continue;
}
const actuallyNewTransactions = entry.transactions.slice(
Math.max(0, knownStart - entry.after),
);
const newAfter =
entry.after +
(actuallyNewTransactions.length - entry.transactions.length);
let newContentEntry = newContentPieces[0]?.new[sessionID as SessionID];
if (!newContentEntry) {
newContentEntry = {
after: newAfter,
lastSignature: entry.lastSignature,
newTransactions: actuallyNewTransactions,
};
newContentPieces[0]!.new[sessionID as SessionID] = newContentEntry;
} else {
newContentEntry.newTransactions.push(...actuallyNewTransactions);
newContentEntry.lastSignature = entry.lastSignature;
}
}
}
return newContentPieces;
}
export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
const ourKnown: CoValueKnownState = {
id,
header: !!chunk.header,
sessions: {},
};
for (const [sessionID, sessionEntries] of Object.entries(
chunk.sessionEntries,
)) {
for (const entry of sessionEntries) {
ourKnown.sessions[sessionID as SessionID] =
entry.after + entry.transactions.length;
}
}
return ourKnown;
}
export function mergeChunks(
chunkA: CoValueChunk,
chunkB: CoValueChunk,
): "nonContigous" | CoValueChunk {
const header = chunkA.header || chunkB.header;
const newSessions = { ...chunkA.sessionEntries };
for (const sessionID in chunkB.sessionEntries) {
// figure out if we can merge the chunks
const sessionEntriesA = chunkA.sessionEntries[sessionID];
const sessionEntriesB = chunkB.sessionEntries[sessionID]!;
if (!sessionEntriesA) {
newSessions[sessionID] = sessionEntriesB;
continue;
}
const lastEntryOfA = sessionEntriesA[sessionEntriesA.length - 1]!;
const firstEntryOfB = sessionEntriesB[0]!;
if (
lastEntryOfA.after + lastEntryOfA.transactions.length ===
firstEntryOfB.after
) {
const newEntries = [];
let bytesSinceLastSignature = 0;
for (const entry of sessionEntriesA.concat(sessionEntriesB)) {
const entryByteLength = entry.transactions.reduce(
(sum, tx) =>
sum +
(tx.privacy === "private"
? tx.encryptedChanges.length
: tx.changes.length),
0,
);
if (
newEntries.length === 0 ||
bytesSinceLastSignature + entryByteLength > MAX_RECOMMENDED_TX_SIZE
) {
newEntries.push({
after: entry.after,
lastSignature: entry.lastSignature,
transactions: entry.transactions,
});
bytesSinceLastSignature = 0;
} else {
const lastNewEntry = newEntries[newEntries.length - 1]!;
lastNewEntry.transactions.push(...entry.transactions);
lastNewEntry.lastSignature = entry.lastSignature;
bytesSinceLastSignature += entry.transactions.length;
}
}
newSessions[sessionID] = newEntries;
} else {
return "nonContigous" as const;
}
}
return { header, sessionEntries: newSessions };
}

View File

@@ -1,531 +0,0 @@
import { CoID, RawCoValue } from "../coValue.js";
import { CoValueHeader, Transaction } from "../coValueCore.js";
import { Signature } from "../crypto/crypto.js";
import { RawCoID } from "../ids.js";
import { logger } from "../logger.js";
import { connectedPeers } from "../streamUtils.js";
import {
CoValueKnownState,
IncomingSyncStream,
NewContentMessage,
OutgoingSyncQueue,
Peer,
} from "../sync.js";
import {
BlockFilename,
FileSystem,
WalEntry,
WalFilename,
readChunk,
readHeader,
textDecoder,
writeBlock,
writeToWal,
} from "./FileSystem.js";
import {
chunkToKnownState,
contentSinceChunk,
mergeChunks,
} from "./chunksAndKnownStates.js";
export type { BlockFilename, WalFilename } from "./FileSystem.js";
const MAX_N_LEVELS = 3;
export type CoValueChunk = {
header?: CoValueHeader;
sessionEntries: {
[sessionID: string]: {
after: number;
lastSignature: Signature;
transactions: Transaction[];
}[];
};
};
export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
currentWal: WH | undefined;
coValues: {
[id: RawCoID]: CoValueChunk | undefined;
};
fileCache: string[] | undefined;
headerCache = new Map<
BlockFilename,
{ [id: RawCoID]: { start: number; length: number } }
>();
blockFileHandles = new Map<
BlockFilename,
Promise<{ handle: RH; size: number }>
>();
constructor(
public fs: FS,
public fromLocalNode: IncomingSyncStream,
public toLocalNode: OutgoingSyncQueue,
) {
this.coValues = {};
this.currentWal = undefined;
let nMsg = 0;
const processMessages = async () => {
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
if (msg.action === "done") {
return;
}
if (msg.action === "content") {
await this.handleNewContent(msg);
} else if (msg.action === "load" || msg.action === "known") {
await this.sendNewContent(msg.id, msg, undefined);
}
} catch (e) {
logger.error(`Error reading from localNode, handling msg`, {
msg,
err: e,
});
}
nMsg++;
}
};
processMessages().catch((e) =>
logger.error("Error in processMessages in storage", { err: e }),
);
setTimeout(
() =>
this.compact().catch((e) => {
logger.error("Error while compacting", { err: e });
}),
20000,
);
}
async sendNewContent(
id: RawCoID,
known: CoValueKnownState | undefined,
asDependencyOf: RawCoID | undefined,
) {
let coValue = this.coValues[id];
if (!coValue) {
coValue = await this.loadCoValue(id, this.fs);
}
if (!coValue) {
this.toLocalNode
.push({
id: id,
action: "known",
header: false,
sessions: {},
asDependencyOf,
})
.catch((e) => logger.error("Error while pushing known", { err: e }));
return;
}
if (!known?.header && coValue.header?.ruleset.type === "ownedByGroup") {
await this.sendNewContent(
coValue.header.ruleset.group,
undefined,
asDependencyOf || id,
);
} else if (!known?.header && coValue.header?.ruleset.type === "group") {
const dependedOnAccountsAndGroups = new Set();
for (const session of Object.values(coValue.sessionEntries)) {
for (const entry of session) {
for (const tx of entry.transactions) {
if (tx.privacy === "trusting") {
const parsedChanges = JSON.parse(tx.changes);
for (const change of parsedChanges) {
if (change.op === "set" && change.key.startsWith("co_")) {
dependedOnAccountsAndGroups.add(change.key);
}
if (
change.op === "set" &&
change.key.startsWith("parent_co_")
) {
dependedOnAccountsAndGroups.add(
change.key.replace("parent_", ""),
);
}
}
}
}
}
}
for (const accountOrGroup of dependedOnAccountsAndGroups) {
await this.sendNewContent(
accountOrGroup as CoID<RawCoValue>,
undefined,
asDependencyOf || id,
);
}
}
const newContentMessages = contentSinceChunk(id, coValue, known).map(
(message) => ({ ...message, asDependencyOf }),
);
const ourKnown: CoValueKnownState = chunkToKnownState(id, coValue);
this.toLocalNode
.push({
action: "known",
...ourKnown,
asDependencyOf,
})
.catch((e) => logger.error("Error while pushing known", { err: e }));
for (const message of newContentMessages) {
if (Object.keys(message.new).length === 0) continue;
this.toLocalNode
.push(message)
.catch((e) =>
logger.error("Error while pushing new content", { err: e }),
);
}
this.coValues[id] = coValue;
}
async withWAL(handler: (wal: WH) => Promise<void>) {
if (!this.currentWal) {
this.currentWal = await this.fs.createFile(
`wal-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`,
);
}
await handler(this.currentWal);
}
async handleNewContent(newContent: NewContentMessage) {
const coValue = this.coValues[newContent.id];
const newContentAsChunk: CoValueChunk = {
header: newContent.header,
sessionEntries: Object.fromEntries(
Object.entries(newContent.new).map(([sessionID, newInSession]) => [
sessionID,
[
{
after: newInSession.after,
lastSignature: newInSession.lastSignature,
transactions: newInSession.newTransactions,
},
],
]),
),
};
if (!coValue) {
if (newContent.header) {
await this.withWAL((wal) =>
writeToWal(wal, this.fs, newContent.id, newContentAsChunk),
);
this.coValues[newContent.id] = newContentAsChunk;
} else {
logger.warn("Incontiguous incoming update for " + newContent.id);
return;
}
} else {
const merged = mergeChunks(coValue, newContentAsChunk);
if (merged === "nonContigous") {
console.warn(
"Non-contigous new content for " + newContent.id,
Object.entries(coValue.sessionEntries).map(([session, entries]) =>
entries.map((entry) => ({
session: session,
after: entry.after,
length: entry.transactions.length,
})),
),
Object.entries(newContentAsChunk.sessionEntries).map(
([session, entries]) =>
entries.map((entry) => ({
session: session,
after: entry.after,
length: entry.transactions.length,
})),
),
);
} else {
await this.withWAL((wal) =>
writeToWal(wal, this.fs, newContent.id, newContentAsChunk),
);
this.coValues[newContent.id] = merged;
}
}
}
async getBlockHandle(
blockFile: BlockFilename,
fs: FS,
): Promise<{ handle: RH; size: number }> {
if (!this.blockFileHandles.has(blockFile)) {
this.blockFileHandles.set(blockFile, fs.openToRead(blockFile));
}
return this.blockFileHandles.get(blockFile)!;
}
async loadCoValue(id: RawCoID, fs: FS): Promise<CoValueChunk | undefined> {
const files = this.fileCache || (await fs.listFiles());
this.fileCache = files;
const blockFiles = (
files.filter((name) => name.startsWith("L")) as BlockFilename[]
).sort();
let result;
for (const blockFile of blockFiles) {
let cachedHeader:
| { [id: RawCoID]: { start: number; length: number } }
| undefined = this.headerCache.get(blockFile);
const { handle, size } = await this.getBlockHandle(blockFile, fs);
if (!cachedHeader) {
cachedHeader = {};
const header = await readHeader(blockFile, handle, size, fs);
for (const entry of header) {
cachedHeader[entry.id] = {
start: entry.start,
length: entry.length,
};
}
this.headerCache.set(blockFile, cachedHeader);
}
const headerEntry = cachedHeader[id];
if (headerEntry) {
const nextChunk = await readChunk(handle, headerEntry, fs);
if (result) {
const merged = mergeChunks(result, nextChunk);
if (merged === "nonContigous") {
console.warn(
"Non-contigous chunks while loading " + id,
result,
nextChunk,
);
} else {
result = merged;
}
} else {
result = nextChunk;
}
}
// await fs.close(handle);
}
return result;
}
async compact() {
const fileNames = await this.fs.listFiles();
const walFiles = fileNames.filter((name) =>
name.startsWith("wal-"),
) as WalFilename[];
walFiles.sort();
const coValues = new Map<RawCoID, CoValueChunk>();
if (walFiles.length === 0) return;
const oldWal = this.currentWal;
this.currentWal = undefined;
if (oldWal) {
await this.fs.close(oldWal);
}
for (const fileName of walFiles) {
const { handle, size }: { handle: RH; size: number } =
await this.fs.openToRead(fileName);
if (size === 0) {
await this.fs.close(handle);
continue;
}
const bytes = await this.fs.read(handle, 0, size);
const decoded = textDecoder.decode(bytes);
const lines = decoded.split("\n");
for (const line of lines) {
if (line.length === 0) continue;
const chunk = JSON.parse(line) as WalEntry;
const existingChunk = coValues.get(chunk.id);
if (existingChunk) {
const merged = mergeChunks(existingChunk, chunk);
if (merged === "nonContigous") {
console.log(
"Non-contigous chunks in " + chunk.id + ", " + fileName,
existingChunk,
chunk,
);
} else {
coValues.set(chunk.id, merged);
}
} else {
coValues.set(chunk.id, chunk);
}
}
await this.fs.close(handle);
}
const highestBlockNumber = fileNames.reduce((acc, name) => {
if (name.startsWith("L" + MAX_N_LEVELS)) {
const num = parseInt(name.split("-")[1]!);
if (num > acc) {
return num;
}
}
return acc;
}, 0);
await writeBlock(coValues, MAX_N_LEVELS, highestBlockNumber + 1, this.fs);
for (const walFile of walFiles) {
await this.fs.removeFile(walFile);
}
this.fileCache = undefined;
const fileNames2 = await this.fs.listFiles();
const blockFiles = (
fileNames2.filter((name) => name.startsWith("L")) as BlockFilename[]
).sort();
const blockFilesByLevelInOrder: {
[level: number]: BlockFilename[];
} = {};
for (const blockFile of blockFiles) {
const level = parseInt(blockFile.split("-")[0]!.slice(1));
if (!blockFilesByLevelInOrder[level]) {
blockFilesByLevelInOrder[level] = [];
}
blockFilesByLevelInOrder[level]!.push(blockFile);
}
for (let level = MAX_N_LEVELS; level > 0; level--) {
const nBlocksDesired = Math.pow(2, level);
const blocksInLevel = blockFilesByLevelInOrder[level];
if (blocksInLevel && blocksInLevel.length > nBlocksDesired) {
const coValues = new Map<RawCoID, CoValueChunk>();
for (const blockFile of blocksInLevel) {
const { handle, size }: { handle: RH; size: number } =
await this.getBlockHandle(blockFile, this.fs);
if (size === 0) {
continue;
}
const header = await readHeader(blockFile, handle, size, this.fs);
for (const entry of header) {
const chunk = await readChunk(handle, entry, this.fs);
const existingChunk = coValues.get(entry.id);
if (existingChunk) {
const merged = mergeChunks(existingChunk, chunk);
if (merged === "nonContigous") {
console.log(
"Non-contigous chunks in " + entry.id + ", " + blockFile,
existingChunk,
chunk,
);
} else {
coValues.set(entry.id, merged);
}
} else {
coValues.set(entry.id, chunk);
}
}
}
let levelBelow = blockFilesByLevelInOrder[level - 1];
if (!levelBelow) {
levelBelow = [];
blockFilesByLevelInOrder[level - 1] = levelBelow;
}
const highestBlockNumberInLevelBelow = levelBelow.reduce(
(acc, name) => {
const num = parseInt(name.split("-")[1]!);
if (num > acc) {
return num;
}
return acc;
},
0,
);
const newBlockName = await writeBlock(
coValues,
level - 1,
highestBlockNumberInLevelBelow + 1,
this.fs,
);
levelBelow.push(newBlockName);
// delete blocks that went into this one
for (const blockFile of blocksInLevel) {
const handle = await this.getBlockHandle(blockFile, this.fs);
await this.fs.close(handle.handle);
await this.fs.removeFile(blockFile);
this.blockFileHandles.delete(blockFile);
}
}
}
setTimeout(
() =>
this.compact().catch((e) => {
logger.error("Error while compacting", { err: e });
}),
5000,
);
}
static asPeer<WH, RH, FS extends FileSystem<WH, RH>>({
fs,
trace,
localNodeName = "local",
}: {
fs: FS;
trace?: boolean;
localNodeName?: string;
}): Peer {
const [localNodeAsPeer, storageAsPeer] = connectedPeers(
localNodeName,
"storage",
{
peer1role: "client",
peer2role: "storage",
trace,
crashOnClose: true,
},
);
new LSMStorage(fs, localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
// return { ...storageAsPeer, priority: 200 };
return storageAsPeer;
}
}

View File

@@ -7,8 +7,8 @@ export function connectedPeers(
peer2id: PeerID,
{
trace = false,
peer1role = "peer",
peer2role = "peer",
peer1role = "client",
peer2role = "client",
crashOnClose = false,
}: {
trace?: boolean;

View File

@@ -78,7 +78,7 @@ export interface Peer {
id: PeerID;
incoming: IncomingSyncStream;
outgoing: OutgoingSyncQueue;
role: "peer" | "server" | "client" | "storage";
role: "server" | "client" | "storage";
priority?: number;
crashOnClose: boolean;
deletePeerStateOnClose?: boolean;
@@ -112,11 +112,6 @@ export function combinedKnownStates(
export class SyncManager {
peers: { [key: PeerID]: PeerState } = {};
local: LocalNode;
requestedSyncs: {
[id: RawCoID]:
| { done: Promise<void>; nRequestsThisTick: number }
| undefined;
} = {};
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
description: "Amount of connected peers",
@@ -162,8 +157,8 @@ export class SyncManager {
);
}
async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
if (peer.erroredCoValues.has(msg.id)) {
handleSyncMessage(msg: SyncMessage, peer: PeerState) {
if (this.local.coValuesStore.get(msg.id).isErroredInPeer(peer.id)) {
logger.warn(
`Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
);
@@ -183,18 +178,17 @@ export class SyncManager {
// TODO: validate
switch (msg.action) {
case "load":
return await this.handleLoad(msg, peer);
return this.handleLoad(msg, peer);
case "known":
if (msg.isCorrection) {
return await this.handleCorrection(msg, peer);
return this.handleCorrection(msg, peer);
} else {
return await this.handleKnownState(msg, peer);
return this.handleKnownState(msg, peer);
}
case "content":
// await new Promise<void>((resolve) => setTimeout(resolve, 0));
return await this.handleNewContent(msg, peer);
return this.handleNewContent(msg, peer);
case "done":
return await this.handleUnsubscribe(msg);
return this.handleUnsubscribe(msg);
default:
throw new Error(
`Unknown message type ${(msg as { action: "string" }).action}`,
@@ -202,14 +196,12 @@ export class SyncManager {
}
}
async sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
const coValue = this.local.expectCoValueLoaded(id);
await Promise.all(
coValue
.getDependedOnCoValues()
.map((id) => this.sendNewContentIncludingDependencies(id, peer)),
);
coValue
.getDependedOnCoValues()
.map((id) => this.sendNewContentIncludingDependencies(id, peer));
const newContentPieces = coValue.newContentSince(
peer.optimisticKnownStates.get(id),
@@ -217,9 +209,7 @@ export class SyncManager {
if (newContentPieces) {
for (const piece of newContentPieces) {
this.trySendToPeer(peer, piece).catch((e: unknown) => {
logger.error("Error sending content piece", { err: e });
});
this.trySendToPeer(peer, piece);
}
peer.toldKnownState.add(id);
@@ -228,15 +218,13 @@ export class SyncManager {
this.trySendToPeer(peer, {
action: "known",
...coValue.knownState(),
}).catch((e: unknown) => {
logger.error("Error sending known state", { err: e });
});
peer.toldKnownState.add(id);
}
}
async startPeerReconciliation(peer: PeerState) {
startPeerReconciliation(peer: PeerState) {
const coValuesOrderedByDependency: CoValueCore[] = [];
const gathered = new Set<string>();
@@ -251,8 +239,8 @@ export class SyncManager {
for (const id of coValue.getDependedOnCoValues()) {
const entry = this.local.coValuesStore.get(id);
if (entry.state.type === "available") {
buildOrderedCoValueList(entry.state.coValue);
if (entry.isAvailable()) {
buildOrderedCoValueList(entry.core);
}
}
@@ -260,23 +248,24 @@ export class SyncManager {
};
for (const entry of this.local.coValuesStore.getValues()) {
switch (entry.state.type) {
case "unavailable":
// If the coValue is unavailable and we never tried this peer
// we try to load it from the peer
if (!peer.toldKnownState.has(entry.id)) {
await entry.loadFromPeers([peer]).catch((e: unknown) => {
logger.error("Error sending load", { err: e });
});
}
break;
case "available":
const coValue = entry.state.coValue;
if (!entry.isAvailable()) {
// If the coValue is unavailable and we never tried this peer
// we try to load it from the peer
if (!peer.toldKnownState.has(entry.id)) {
peer.toldKnownState.add(entry.id);
this.trySendToPeer(peer, {
action: "load",
header: false,
id: entry.id,
sessions: {},
});
}
} else {
const coValue = entry.core;
// Build the list of coValues ordered by dependency
// so we can send the load message in the correct order
buildOrderedCoValueList(coValue);
break;
// Build the list of coValues ordered by dependency
// so we can send the load message in the correct order
buildOrderedCoValueList(coValue);
}
// Fill the missing known states with empty known states
@@ -296,33 +285,15 @@ export class SyncManager {
this.trySendToPeer(peer, {
action: "load",
...coValue.knownState(),
}).catch((e: unknown) => {
logger.error("Error sending load", { err: e });
});
}
}
nextPeer: Map<PeerID, Peer> = new Map();
async addPeer(peer: Peer) {
const prevPeer = this.peers[peer.id];
if (prevPeer) {
// Assign to nextPeer to check against race conditions
prevPeer.nextPeer = peer;
if (!prevPeer.closed) {
prevPeer.gracefulShutdown();
}
// Wait for the previous peer to finish processing the incoming messages
await prevPeer.incomingMessagesProcessingPromise?.catch((e) => {});
// If another peer was added in the meantime, we close this peer
if (prevPeer.nextPeer !== peer) {
peer.outgoing.close();
return;
}
if (prevPeer && !prevPeer.closed) {
prevPeer.gracefulShutdown();
}
const peerState = new PeerState(peer, prevPeer?.knownStates);
@@ -341,8 +312,8 @@ export class SyncManager {
}
peerState
.processIncomingMessages(async (msg) => {
await this.handleSyncMessage(msg, peerState);
.processIncomingMessages((msg) => {
this.handleSyncMessage(msg, peerState);
})
.then(() => {
if (peer.crashOnClose) {
@@ -389,7 +360,7 @@ export class SyncManager {
* - The peer known state is stored as-is instead of being merged
* - The load message always replies with a known state message
*/
async handleLoad(msg: LoadMessage, peer: PeerState) {
handleLoad(msg: LoadMessage, peer: PeerState) {
/**
* We use the msg sessions as source of truth for the known states
*
@@ -399,7 +370,10 @@ export class SyncManager {
peer.setKnownState(msg.id, knownStateIn(msg));
const entry = this.local.coValuesStore.get(msg.id);
if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
if (
entry.highLevelState === "unknown" ||
entry.highLevelState === "unavailable"
) {
const eligiblePeers = this.getServerAndStoragePeers(peer.id);
if (eligiblePeers.length === 0) {
@@ -413,8 +387,6 @@ export class SyncManager {
id: msg.id,
header: false,
sessions: {},
}).catch((e) => {
logger.error("Error sending known state back", { err: e });
});
return;
@@ -426,7 +398,7 @@ export class SyncManager {
}
}
if (entry.state.type === "loading") {
if (entry.highLevelState === "loading") {
// We need to return from handleLoad immediately and wait for the CoValue to be loaded
// in a new task, otherwise we might block further incoming content messages that would
// resolve the CoValue as available. This can happen when we receive fresh
@@ -442,22 +414,20 @@ export class SyncManager {
id: msg.id,
header: false,
sessions: {},
}).catch((e) => {
logger.error("Error sending known state back", { err: e });
});
return;
}
await this.sendNewContentIncludingDependencies(msg.id, peer);
this.sendNewContentIncludingDependencies(msg.id, peer);
})
.catch((e) => {
logger.error("Error loading coValue in handleLoad loading state", {
err: e,
});
});
} else if (entry.state.type === "available") {
await this.sendNewContentIncludingDependencies(msg.id, peer);
} else if (entry.isAvailable()) {
this.sendNewContentIncludingDependencies(msg.id, peer);
} else {
this.trySendToPeer(peer, {
action: "known",
@@ -468,28 +438,21 @@ export class SyncManager {
}
}
async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
handleKnownState(msg: KnownStateMessage, peer: PeerState) {
const entry = this.local.coValuesStore.get(msg.id);
peer.combineWith(msg.id, knownStateIn(msg));
// The header is a boolean value that tells us if the other peer do have information about the header.
// If it's false in this point it means that the coValue is unavailable on the other peer.
if (entry.state.type !== "available") {
const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
if (!availableOnPeer) {
entry.dispatch({
type: "not-found-in-peer",
peerId: peer.id,
});
}
return;
if (!availableOnPeer) {
entry.markNotFoundInPeer(peer.id);
}
if (entry.state.type === "available") {
await this.sendNewContentIncludingDependencies(msg.id, peer);
if (entry.isAvailable()) {
this.sendNewContentIncludingDependencies(msg.id, peer);
}
}
@@ -506,12 +469,12 @@ export class SyncManager {
}
}
async handleNewContent(msg: NewContentMessage, peer: PeerState) {
handleNewContent(msg: NewContentMessage, peer: PeerState) {
const entry = this.local.coValuesStore.get(msg.id);
let coValue: CoValueCore;
if (entry.state.type !== "available") {
if (!entry.isAvailable()) {
if (!msg.header) {
this.trySendToPeer(peer, {
action: "known",
@@ -519,12 +482,6 @@ export class SyncManager {
id: msg.id,
header: false,
sessions: {},
}).catch((e) => {
logger.error("Error sending known state correction", {
peerId: peer.id,
peerRole: peer.role,
err: e,
});
});
return;
}
@@ -533,12 +490,9 @@ export class SyncManager {
coValue = new CoValueCore(msg.header, this.local);
entry.dispatch({
type: "available",
coValue,
});
entry.markAvailable(coValue, peer.id);
} else {
coValue = entry.state.coValue;
coValue = entry.core;
}
let invalidStateAssumed = false;
@@ -581,7 +535,7 @@ export class SyncManager {
id: msg.id,
err: result.error,
});
peer.erroredCoValues.set(msg.id, result.error);
entry.markErrored(peer.id, result.error);
continue;
}
@@ -600,12 +554,6 @@ export class SyncManager {
action: "known",
isCorrection: true,
...coValue.knownState(),
}).catch((e) => {
logger.error("Error sending known state correction", {
peerId: peer.id,
peerRole: peer.role,
err: e,
});
});
peer.toldKnownState.add(msg.id);
} else {
@@ -619,12 +567,6 @@ export class SyncManager {
this.trySendToPeer(peer, {
action: "known",
...coValue.knownState(),
}).catch((e: unknown) => {
logger.error("Error sending known state", {
peerId: peer.id,
peerRole: peer.role,
err: e,
});
});
peer.toldKnownState.add(msg.id);
}
@@ -634,50 +576,54 @@ export class SyncManager {
* response to the peers that are waiting for confirmation that a coValue is
* fully synced
*/
this.syncCoValue(coValue);
this.requestCoValueSync(coValue);
}
async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
handleCorrection(msg: KnownStateMessage, peer: PeerState) {
peer.setKnownState(msg.id, knownStateIn(msg));
return this.sendNewContentIncludingDependencies(msg.id, peer);
}
handleUnsubscribe(_msg: DoneMessage) {
throw new Error("Method not implemented.");
}
handleUnsubscribe(_msg: DoneMessage) {}
async syncCoValue(coValue: CoValueCore) {
if (this.requestedSyncs[coValue.id]) {
this.requestedSyncs[coValue.id]!.nRequestsThisTick++;
return this.requestedSyncs[coValue.id]!.done;
requestedSyncs = new Map<RawCoID, Promise<void>>();
async requestCoValueSync(coValue: CoValueCore) {
const promise = this.requestedSyncs.get(coValue.id);
if (promise) {
return promise;
} else {
const done = new Promise<void>((resolve) => {
queueMicrotask(async () => {
delete this.requestedSyncs[coValue.id];
await this.actuallySyncCoValue(coValue);
const promise = new Promise<void>((resolve) => {
queueMicrotask(() => {
this.requestedSyncs.delete(coValue.id);
this.syncCoValue(coValue);
resolve();
});
});
const entry = {
done,
nRequestsThisTick: 1,
};
this.requestedSyncs[coValue.id] = entry;
return done;
this.requestedSyncs.set(coValue.id, promise);
return promise;
}
}
async actuallySyncCoValue(coValue: CoValueCore) {
async syncCoValue(coValue: CoValueCore) {
const entry = this.local.coValuesStore.get(coValue.id);
for (const peer of this.peersInPriorityOrder()) {
if (peer.closed) continue;
if (peer.erroredCoValues.has(coValue.id)) continue;
if (entry.isErroredInPeer(peer.id)) continue;
if (peer.optimisticKnownStates.has(coValue.id)) {
await this.sendNewContentIncludingDependencies(coValue.id, peer);
} else if (peer.isServerOrStoragePeer()) {
await this.sendNewContentIncludingDependencies(coValue.id, peer);
// Only subscribed CoValues are synced to clients
if (
peer.role === "client" &&
!peer.optimisticKnownStates.has(coValue.id)
) {
continue;
}
this.sendNewContentIncludingDependencies(coValue.id, peer);
}
for (const peer of this.getPeers()) {
@@ -726,7 +672,8 @@ export class SyncManager {
const coValues = this.local.coValuesStore.getValues();
const validCoValues = Array.from(coValues).filter(
(coValue) =>
coValue.state.type === "available" || coValue.state.type === "loading",
coValue.highLevelState === "available" ||
coValue.highLevelState === "loading",
);
return Promise.all(

View File

@@ -2,11 +2,12 @@ import { describe, expect, test, vi } from "vitest";
import { PeerState } from "../PeerState.js";
import { CO_VALUE_PRIORITY } from "../priority.js";
import { CoValueKnownState, Peer, SyncMessage } from "../sync.js";
import { waitFor } from "./testUtils.js";
function setup() {
const mockPeer: Peer = {
id: "test-peer",
role: "peer",
role: "client",
priority: 1,
crashOnClose: false,
incoming: (async function* () {})(),
@@ -62,13 +63,19 @@ describe("PeerState", () => {
});
});
const message1 = peerState.pushOutgoingMessage({
peerState.pushOutgoingMessage({
action: "content",
id: "co_z1",
new: {},
priority: CO_VALUE_PRIORITY.HIGH,
});
const message2 = peerState.pushOutgoingMessage({
peerState.pushOutgoingMessage({
action: "content",
id: "co_z1",
new: {},
priority: CO_VALUE_PRIORITY.HIGH,
});
peerState.pushOutgoingMessage({
action: "content",
id: "co_z1",
new: {},
@@ -77,14 +84,21 @@ describe("PeerState", () => {
peerState.gracefulShutdown();
await Promise.allSettled([message1, message2]);
await waitFor(() => {
expect(peerState.isProcessing()).toBe(false);
});
await expect(message1).resolves.toBe(undefined);
await expect(message2).resolves.toBe(undefined);
expect(mockPeer.outgoing.push).toHaveBeenCalledTimes(1);
});
test("should schedule outgoing messages based on their priority", async () => {
const { peerState } = setup();
const { peerState, mockPeer } = setup();
mockPeer.outgoing.push = vi.fn().mockImplementation((message) => {
return new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
});
const loadMessage: SyncMessage = {
action: "load",
@@ -111,14 +125,14 @@ describe("PeerState", () => {
priority: CO_VALUE_PRIORITY.LOW,
};
const promises = [
peerState.pushOutgoingMessage(contentMessageLow),
peerState.pushOutgoingMessage(contentMessageMid),
peerState.pushOutgoingMessage(contentMessageHigh),
peerState.pushOutgoingMessage(loadMessage),
];
peerState.pushOutgoingMessage(contentMessageLow);
peerState.pushOutgoingMessage(contentMessageMid);
peerState.pushOutgoingMessage(contentMessageHigh);
peerState.pushOutgoingMessage(loadMessage);
await Promise.all(promises);
await waitFor(() => {
expect(peerState.isProcessing()).toBe(false);
});
// The first message is pushed directly, the other three are queued because are waiting
// for the first push to be completed.

View File

@@ -157,8 +157,7 @@ describe("PriorityBasedMessageQueue", () => {
sessions: {},
};
void queue.push(message);
const pulledEntry = queue.pull();
expect(pulledEntry?.msg).toEqual(message);
expect(queue.pull()).toEqual(message);
});
test("should push message with specified priority", async () => {
@@ -170,8 +169,7 @@ describe("PriorityBasedMessageQueue", () => {
priority: CO_VALUE_PRIORITY.HIGH,
};
void queue.push(message);
const pulledEntry = queue.pull();
expect(pulledEntry?.msg).toEqual(message);
expect(queue.pull()).toEqual(message);
});
test("should pull messages in priority order", async () => {
@@ -199,45 +197,13 @@ describe("PriorityBasedMessageQueue", () => {
void queue.push(mediumPriorityMsg);
void queue.push(highPriorityMsg);
expect(queue.pull()?.msg).toEqual(highPriorityMsg);
expect(queue.pull()?.msg).toEqual(mediumPriorityMsg);
expect(queue.pull()?.msg).toEqual(lowPriorityMsg);
expect(queue.pull()).toEqual(highPriorityMsg);
expect(queue.pull()).toEqual(mediumPriorityMsg);
expect(queue.pull()).toEqual(lowPriorityMsg);
});
test("should return undefined when pulling from empty queue", () => {
const { queue } = setup();
expect(queue.pull()).toBeUndefined();
});
test("should resolve promise when message is pulled", async () => {
const { queue } = setup();
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
const pushPromise = queue.push(message);
const pulledEntry = queue.pull();
pulledEntry?.resolve();
await expect(pushPromise).resolves.toBeUndefined();
});
test("should reject promise when message is rejected", async () => {
const { queue } = setup();
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
const pushPromise = queue.push(message);
const pulledEntry = queue.pull();
pulledEntry?.reject(new Error("Test error"));
await expect(pushPromise).rejects.toThrow("Test error");
});
});

View File

@@ -7,8 +7,6 @@ import { connectedPeers } from "../streamUtils.js";
import { emptyKnownState } from "../sync.js";
import {
SyncMessagesLog,
blockMessageTypeOnOutgoingPeer,
createTestNode,
loadCoValueOrFail,
setupTestNode,
waitFor,
@@ -37,7 +35,7 @@ describe("SyncStateManager", () => {
const updateSpy: GlobalSyncStateListenerCallback = vi.fn();
const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
await client.node.syncManager.actuallySyncCoValue(map.core);
await client.node.syncManager.syncCoValue(map.core);
expect(updateSpy).toHaveBeenCalledWith(
peerState.id,
@@ -97,7 +95,7 @@ describe("SyncStateManager", () => {
unsubscribe2();
});
await client.node.syncManager.actuallySyncCoValue(map.core);
await client.node.syncManager.syncCoValue(map.core);
expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
emptyKnownState(map.core.id),
@@ -132,7 +130,7 @@ describe("SyncStateManager", () => {
const map = group.createMap();
map.set("key1", "value1", "trusting");
await client.node.syncManager.actuallySyncCoValue(map.core);
await client.node.syncManager.syncCoValue(map.core);
const subscriptionManager = client.node.syncManager.syncState;
@@ -173,7 +171,7 @@ describe("SyncStateManager", () => {
unsubscribe1();
unsubscribe2();
await client.node.syncManager.actuallySyncCoValue(map.core);
await client.node.syncManager.syncCoValue(map.core);
anyUpdateSpy.mockClear();
@@ -217,9 +215,6 @@ describe("SyncStateManager", () => {
const mapOnServer = await loadCoValueOrFail(serverNode, map.id);
// Block the content messages so the client won't fully sync immediately
const outgoing = blockMessageTypeOnOutgoingPeer(peerOnServer, "content");
mapOnServer.set("key2", "value2", "trusting");
expect(
@@ -236,9 +231,6 @@ describe("SyncStateManager", () => {
),
).toEqual({ uploaded: false });
await outgoing.sendBlockedMessages();
outgoing.unblock();
await mapOnServer.core.waitForSync();
expect(

View File

@@ -221,16 +221,3 @@ test("Items prepended to start appear with latest first", () => {
expect(content.toJSON()).toEqual(["third", "second", "first"]);
});
test("should handle large lists", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const group = node.createGroup();
const coValue = group.createList();
for (let i = 0; i < 8_000; i++) {
coValue.append(`item ${i}`, undefined, "trusting");
}
expect(coValue.toJSON().length).toEqual(8_000);
});

View File

@@ -40,10 +40,10 @@ describe("CoValueState", () => {
const mockCoValueId = "co_test123" as RawCoID;
test("should create unknown state", async () => {
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
expect(state.id).toBe(mockCoValueId);
expect(state.state.type).toBe("unknown");
expect(state.highLevelState).toBe("unknown");
expect(
await metricReader.getMetricValue("jazz.covalues.loaded", {
state: "unknown",
@@ -52,11 +52,14 @@ describe("CoValueState", () => {
});
test("should create loading state", async () => {
const peerIds = ["peer1", "peer2"];
const state = CoValueState.Loading(mockCoValueId, peerIds);
const state = new CoValueState(mockCoValueId);
state.loadFromPeers([
createMockPeerState({ id: "peer1", role: "server" }),
createMockPeerState({ id: "peer2", role: "server" }),
]);
expect(state.id).toBe(mockCoValueId);
expect(state.state.type).toBe("loading");
expect(state.highLevelState).toBe("loading");
expect(
await metricReader.getMetricValue("jazz.covalues.loaded", {
state: "loading",
@@ -66,11 +69,12 @@ describe("CoValueState", () => {
test("should create available state", async () => {
const mockCoValue = createMockCoValueCore(mockCoValueId);
const state = CoValueState.Available(mockCoValue);
const state = new CoValueState(mockCoValueId);
state.internalMarkMagicallyAvailable(mockCoValue);
expect(state.id).toBe(mockCoValueId);
assert(state.state.type === "available");
expect(state.state.coValue).toBe(mockCoValue);
expect(state.highLevelState).toBe("available");
expect(state.core).toBe(mockCoValue);
await expect(state.getCoValue()).resolves.toEqual(mockCoValue);
expect(
await metricReader.getMetricValue("jazz.covalues.loaded", {
@@ -81,7 +85,11 @@ describe("CoValueState", () => {
test("should handle found action", async () => {
const mockCoValue = createMockCoValueCore(mockCoValueId);
const state = CoValueState.Loading(mockCoValueId, ["peer1", "peer2"]);
const state = new CoValueState(mockCoValueId);
state.loadFromPeers([
createMockPeerState({ id: "peer1", role: "server" }),
createMockPeerState({ id: "peer2", role: "server" }),
]);
expect(
await metricReader.getMetricValue("jazz.covalues.loaded", {
@@ -96,10 +104,7 @@ describe("CoValueState", () => {
const stateValuePromise = state.getCoValue();
state.dispatch({
type: "available",
coValue: mockCoValue,
});
state.internalMarkMagicallyAvailable(mockCoValue);
const result = await state.getCoValue();
expect(result).toBe(mockCoValue);
@@ -117,17 +122,6 @@ describe("CoValueState", () => {
).toBe(0);
});
test("should ignore actions when not in loading state", () => {
const state = CoValueState.Unknown(mockCoValueId);
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
expect(state.state.type).toBe("unknown");
});
test("should retry loading from peers when unsuccessful", async () => {
vi.useFakeTimers();
@@ -137,10 +131,7 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
state.markNotFoundInPeer("peer1");
},
);
const peer2 = createMockPeerState(
@@ -149,15 +140,12 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer2",
});
state.markNotFoundInPeer("peer2");
},
);
const mockPeers = [peer1, peer2] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
// Should attempt CO_VALUE_LOADING_CONFIG.MAX_RETRIES retries
@@ -173,7 +161,7 @@ describe("CoValueState", () => {
expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
);
expect(state.state.type).toBe("unavailable");
expect(state.highLevelState).toBe("unavailable");
await expect(state.getCoValue()).resolves.toBe("unavailable");
vi.useRealTimers();
@@ -188,11 +176,7 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
peer1.erroredCoValues.set(mockCoValueId, new Error("test") as any);
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
state.markErrored("peer1", {} as any);
},
);
const peer2 = createMockPeerState(
@@ -201,16 +185,13 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer2",
});
state.markNotFoundInPeer("peer2");
},
);
const mockPeers = [peer1, peer2] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
// Should attempt CO_VALUE_LOADING_CONFIG.MAX_RETRIES retries
@@ -224,7 +205,7 @@ describe("CoValueState", () => {
expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
);
expect(state.state.type).toBe("unavailable");
expect(state.highLevelState).toBe("unavailable");
await expect(state.getCoValue()).resolves.toBe("unavailable");
vi.useRealTimers();
@@ -239,10 +220,7 @@ describe("CoValueState", () => {
role: "storage",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
state.markNotFoundInPeer("peer1");
},
);
const peer2 = createMockPeerState(
@@ -251,15 +229,12 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer2",
});
state.markNotFoundInPeer("peer2");
},
);
const mockPeers = [peer1, peer2] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
// Should attempt CO_VALUE_LOADING_CONFIG.MAX_RETRIES retries
@@ -273,7 +248,7 @@ describe("CoValueState", () => {
expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
);
expect(state.state.type).toBe("unavailable");
expect(state.highLevelState).toBe("unavailable");
await expect(state.getCoValue()).resolves.toEqual("unavailable");
vi.useRealTimers();
@@ -293,17 +268,11 @@ describe("CoValueState", () => {
},
async () => {
retries++;
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
state.markNotFoundInPeer("peer1");
if (retries === 2) {
setTimeout(() => {
state.dispatch({
type: "available",
coValue: createMockCoValueCore(mockCoValueId),
});
state.markAvailable(createMockCoValueCore(mockCoValueId), "peer1");
}, 100);
}
},
@@ -311,7 +280,7 @@ describe("CoValueState", () => {
const mockPeers = [peer1] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
// Should attempt CO_VALUE_LOADING_CONFIG.MAX_RETRIES retries
@@ -322,7 +291,7 @@ describe("CoValueState", () => {
await loadPromise;
expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(2);
expect(state.state.type).toBe("available");
expect(state.highLevelState).toBe("available");
await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
vi.useRealTimers();
});
@@ -336,16 +305,13 @@ describe("CoValueState", () => {
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
state.markNotFoundInPeer("peer1");
},
);
const mockPeers = [peer1] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
// Should attempt CO_VALUE_LOADING_CONFIG.MAX_RETRIES retries
@@ -353,17 +319,14 @@ describe("CoValueState", () => {
await vi.runAllTimersAsync();
}
state.dispatch({
type: "available",
coValue: createMockCoValueCore(mockCoValueId),
});
state.internalMarkMagicallyAvailable(createMockCoValueCore(mockCoValueId));
await loadPromise;
expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
);
expect(state.state.type).toBe("available");
expect(state.highLevelState).toBe("available");
await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
vi.useRealTimers();
@@ -383,22 +346,17 @@ describe("CoValueState", () => {
},
async () => {
if (run > 2) {
state.dispatch({
type: "available",
coValue: createMockCoValueCore(mockCoValueId),
});
state.markAvailable(createMockCoValueCore(mockCoValueId), "peer1");
} else {
state.markNotFoundInPeer("peer1");
run++;
}
state.dispatch({
type: "not-found-in-peer",
peerId: "peer1",
});
run++;
},
);
const mockPeers = [peer1] as unknown as PeerState[];
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers(mockPeers);
for (let i = 0; i < CO_VALUE_LOADING_CONFIG.MAX_RETRIES; i++) {
@@ -407,7 +365,7 @@ describe("CoValueState", () => {
await loadPromise;
expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(3);
expect(state.state.type).toBe("available");
expect(state.highLevelState).toBe("available");
await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
vi.useRealTimers();
@@ -424,26 +382,20 @@ describe("CoValueState", () => {
role: "storage",
},
async () => {
state.dispatch({
type: "available",
coValue: mockCoValue,
});
state.markAvailable(mockCoValue, "peer1");
},
);
const peer2 = createMockPeerState(
{
id: "peer1",
id: "peer2",
role: "server",
},
async () => {
state.dispatch({
type: "not-found-in-peer",
peerId: "peer2",
});
state.markNotFoundInPeer("peer2");
},
);
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers([peer1, peer2]);
for (let i = 0; i < CO_VALUE_LOADING_CONFIG.MAX_RETRIES; i++) {
@@ -457,7 +409,7 @@ describe("CoValueState", () => {
action: "load",
...mockCoValue.knownState(),
});
expect(state.state.type).toBe("available");
expect(state.highLevelState).toBe("available");
await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
vi.useRealTimers();
@@ -479,20 +431,17 @@ describe("CoValueState", () => {
);
const peer2 = createMockPeerState(
{
id: "peer1",
id: "peer2",
role: "server",
},
async () => {
state.dispatch({
type: "available",
coValue: mockCoValue,
});
state.markAvailable(mockCoValue, "peer2");
},
);
peer1.closed = true;
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers([peer1, peer2]);
for (let i = 0; i < CO_VALUE_LOADING_CONFIG.MAX_RETRIES; i++) {
@@ -503,7 +452,7 @@ describe("CoValueState", () => {
expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(0);
expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(1);
expect(state.state.type).toBe("available");
expect(state.highLevelState).toBe("available");
await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
vi.useRealTimers();
@@ -520,7 +469,7 @@ describe("CoValueState", () => {
async () => {},
);
const state = CoValueState.Unknown(mockCoValueId);
const state = new CoValueState(mockCoValueId);
const loadPromise = state.loadFromPeers([peer1]);
for (let i = 0; i < CO_VALUE_LOADING_CONFIG.MAX_RETRIES * 2; i++) {
@@ -532,7 +481,7 @@ describe("CoValueState", () => {
CO_VALUE_LOADING_CONFIG.MAX_RETRIES,
);
expect(state.state.type).toBe("unavailable");
expect(state.highLevelState).toBe("unavailable");
await expect(state.getCoValue()).resolves.toEqual("unavailable");
vi.useRealTimers();

View File

@@ -492,8 +492,8 @@ describe("writeOnly", () => {
);
node2.node.coValuesStore.coValues.delete(map.id);
expect(node2.node.coValuesStore.get(map.id)).toEqual(
CoValueState.Unknown(map.id),
expect(node2.node.coValuesStore.get(map.id)?.highLevelState).toBe(
"unknown",
);
const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);

View File

@@ -1,4 +1,4 @@
import { CoValueCore } from "../exports";
import { CoValueCore, LocalNode, RawControlledAccount } from "../exports";
import {
CoValueKnownState,
NewContentMessage,
@@ -63,6 +63,17 @@ export function toSimplifiedMessages(
return messages.map((m) => toDebugString(m.from, m.to, m.msg));
}
export function nodeRelatedKnownCoValues(node: LocalNode, name: string) {
const account = node.account as RawControlledAccount;
const profileID = account.get("profile");
const profile = profileID && node.expectCoValueLoaded(profileID);
return {
[`${name}Account`]: account,
[`${name}Profile`]: profile,
[`${name}ProfileGroup`]: profile?.getGroup().core,
};
}
export function debugMessages(
coValues: Record<string, CoValueCore>,
messages: {

View File

@@ -74,8 +74,8 @@ describe("loading coValues from server", () => {
"server -> client | CONTENT ParentGroup header: true new: After: 0 New: 6",
"client -> server | KNOWN ParentGroup sessions: header/6",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Map sessions: header/1",
]
`);
@@ -118,6 +118,8 @@ describe("loading coValues from server", () => {
"client -> server | LOAD Map sessions: header/1",
"server -> client | CONTENT Map header: false new: After: 1 New: 1",
"client -> server | KNOWN Map sessions: header/2",
"server -> client | CONTENT Map header: false new: After: 1 New: 1",
"client -> server | KNOWN Map sessions: header/2",
]
`);
});
@@ -161,9 +163,11 @@ describe("loading coValues from server", () => {
"server -> client | KNOWN Group sessions: header/5",
"client -> server | LOAD Map sessions: header/2",
"server -> client | CONTENT Map header: false new: After: 1 New: 1",
"client -> server | KNOWN Map sessions: header/3",
"client -> server | CONTENT Map header: false new: After: 0 New: 1",
"server -> client | CONTENT Map header: false new: After: 1 New: 1",
"client -> server | KNOWN Map sessions: header/3",
"server -> client | KNOWN Map sessions: header/3",
"client -> server | KNOWN Map sessions: header/3",
]
`);
});
@@ -288,36 +292,36 @@ describe("loading coValues from server", () => {
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"client -> server | KNOWN Group sessions: header/5",
"server -> client | CONTENT Map header: true new: ",
"server -> client | CONTENT Map header: false new: After: 0 New: 73",
"client -> server | KNOWN Map sessions: header/0",
"server -> client | CONTENT Map header: false new: After: 73 New: 73",
"server -> client | CONTENT Map header: false new: After: 146 New: 73",
"server -> client | CONTENT Map header: false new: After: 0 New: 73",
"client -> server | KNOWN Map sessions: header/73",
"server -> client | CONTENT Map header: false new: After: 219 New: 73",
"server -> client | CONTENT Map header: false new: After: 292 New: 73",
"server -> client | CONTENT Map header: false new: After: 73 New: 73",
"client -> server | KNOWN Map sessions: header/146",
"server -> client | CONTENT Map header: false new: After: 365 New: 73",
"server -> client | CONTENT Map header: false new: After: 438 New: 73",
"server -> client | CONTENT Map header: false new: After: 146 New: 73",
"client -> server | KNOWN Map sessions: header/219",
"server -> client | CONTENT Map header: false new: After: 511 New: 73",
"server -> client | CONTENT Map header: false new: After: 584 New: 73",
"server -> client | CONTENT Map header: false new: After: 219 New: 73",
"client -> server | KNOWN Map sessions: header/292",
"server -> client | CONTENT Map header: false new: After: 657 New: 73",
"server -> client | CONTENT Map header: false new: After: 730 New: 73",
"server -> client | CONTENT Map header: false new: After: 292 New: 73",
"client -> server | KNOWN Map sessions: header/365",
"server -> client | CONTENT Map header: false new: After: 803 New: 73",
"server -> client | CONTENT Map header: false new: After: 876 New: 73",
"server -> client | CONTENT Map header: false new: After: 365 New: 73",
"client -> server | KNOWN Map sessions: header/438",
"server -> client | CONTENT Map header: false new: After: 949 New: 73",
"server -> client | CONTENT Map header: false new: After: 1022 New: 2",
"server -> client | CONTENT Map header: false new: After: 438 New: 73",
"client -> server | KNOWN Map sessions: header/511",
"server -> client | CONTENT Map header: false new: After: 511 New: 73",
"client -> server | KNOWN Map sessions: header/584",
"server -> client | CONTENT Map header: false new: After: 584 New: 73",
"client -> server | KNOWN Map sessions: header/657",
"server -> client | CONTENT Map header: false new: After: 657 New: 73",
"client -> server | KNOWN Map sessions: header/730",
"server -> client | CONTENT Map header: false new: After: 730 New: 73",
"client -> server | KNOWN Map sessions: header/803",
"server -> client | CONTENT Map header: false new: After: 803 New: 73",
"client -> server | KNOWN Map sessions: header/876",
"server -> client | CONTENT Map header: false new: After: 876 New: 73",
"client -> server | KNOWN Map sessions: header/949",
"server -> client | CONTENT Map header: false new: After: 949 New: 73",
"client -> server | KNOWN Map sessions: header/1022",
"server -> client | CONTENT Map header: false new: After: 1022 New: 2",
"client -> server | KNOWN Map sessions: header/1024",
]
`);

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