Compare commits
55 Commits
jazz-react
...
jazz-brows
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ea30a6f8 | ||
|
|
f4cbe395d5 | ||
|
|
c59fb5dc1f | ||
|
|
c712ef28e8 | ||
|
|
c62a4a1c69 | ||
|
|
d28ce598e2 | ||
|
|
14475991c8 | ||
|
|
15d9ec4b38 | ||
|
|
f911545ae3 | ||
|
|
ad71530cc0 | ||
|
|
c33c02691f | ||
|
|
51c19770a8 | ||
|
|
5c2c7d4188 | ||
|
|
334d27d53d | ||
|
|
a5396a42ce | ||
|
|
5cfe38d547 | ||
|
|
3f7aa34726 | ||
|
|
008750d401 | ||
|
|
72708f82ea | ||
|
|
30f65f1c91 | ||
|
|
67d55ce0ee | ||
|
|
e887f37713 | ||
|
|
82a515d493 | ||
|
|
bd94012507 | ||
|
|
b675249960 | ||
|
|
05198e4181 | ||
|
|
ec9cb40fa4 | ||
|
|
dafea6039b | ||
|
|
fae9b521b8 | ||
|
|
ec1e2e4539 | ||
|
|
9550dcd6e7 | ||
|
|
4547525579 | ||
|
|
856ba0c1fa | ||
|
|
29e05c4ad4 | ||
|
|
65719f21a3 | ||
|
|
05ff90c3c4 | ||
|
|
07408970bd | ||
|
|
4ba3ea6b4e | ||
|
|
c30fb098fe | ||
|
|
a703bc3102 | ||
|
|
18dc96c7b1 | ||
|
|
e9e7f45e02 | ||
|
|
49fb6311ad | ||
|
|
cdc5cbd6d6 | ||
|
|
f55097c480 | ||
|
|
e9695fa2eb | ||
|
|
0c11110567 | ||
|
|
f2db858221 | ||
|
|
a362cbba51 | ||
|
|
39c2586d3b | ||
|
|
e5eed7bd35 | ||
|
|
39ae497153 | ||
|
|
e0b5df7f9e | ||
|
|
54b2907f08 | ||
|
|
f3e0b1ed74 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.98",
|
||||
"version": "1.0.102",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.82",
|
||||
"version": "0.0.86",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.180",
|
||||
"version": "0.0.184",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.79",
|
||||
"version": "0.0.83",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.62",
|
||||
"version": "0.0.66",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.23",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.24",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.76",
|
||||
"version": "0.0.80",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.130",
|
||||
"version": "0.0.134",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multi-cursors",
|
||||
"private": true,
|
||||
"version": "0.0.72",
|
||||
"version": "0.0.76",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.24",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.101",
|
||||
"version": "0.0.105",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.72",
|
||||
"version": "0.0.76",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.66",
|
||||
"version": "0.0.70",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.77",
|
||||
"version": "0.0.81",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.74",
|
||||
"version": "0.0.78",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.98",
|
||||
"version": "0.0.102",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.196",
|
||||
"version": "0.0.200",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.76",
|
||||
"version": "0.0.80",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext",
|
||||
"private": true,
|
||||
"version": "0.0.66",
|
||||
"version": "0.0.70",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.80",
|
||||
"version": "0.0.84",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.195",
|
||||
"version": "0.0.199",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.74",
|
||||
"version": "0.0.78",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
10
homepage/homepage/components/docs/Framework.tsx
Normal file
10
homepage/homepage/components/docs/Framework.tsx
Normal 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}</>;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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($|\/|\\)/];
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,6 @@ export class RawCoPlainText<
|
||||
}
|
||||
this.core.makeTransaction(ops, privacy);
|
||||
|
||||
this.processNewTransactions();
|
||||
this.rebuildFromCore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ export function connectedPeers(
|
||||
peer2id: PeerID,
|
||||
{
|
||||
trace = false,
|
||||
peer1role = "peer",
|
||||
peer2role = "peer",
|
||||
peer1role = "client",
|
||||
peer2role = "client",
|
||||
crashOnClose = false,
|
||||
}: {
|
||||
trace?: boolean;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user