Compare commits
19 Commits
jazz-richt
...
jazz-inspe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85604ec4c5 | ||
|
|
de783063e2 | ||
|
|
9681691701 | ||
|
|
6c7ae1faee | ||
|
|
12e9837858 | ||
|
|
422dbc4222 | ||
|
|
e7ccb2c054 | ||
|
|
2f7046002d | ||
|
|
20c1588249 | ||
|
|
f3d3d4dc5d | ||
|
|
3135d711d4 | ||
|
|
14ad9622ea | ||
|
|
0fee2aa21b | ||
|
|
1e6581cd68 | ||
|
|
aaacaf0130 | ||
|
|
7dcca057e7 | ||
|
|
63570520a3 | ||
|
|
aeed9595ae | ||
|
|
6755e28d0f |
10
.github/ISSUE_TEMPLATE/docs-request.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/docs-request.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Docs request
|
||||
about: Allow people to quickly report issues & improvements for the docs
|
||||
title: 'Docs: '
|
||||
labels: docs, requested
|
||||
assignees: bensleveritt
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# chat-rn-expo-clerk
|
||||
|
||||
## 1.0.119
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-react-native-media-images@0.13.28
|
||||
|
||||
## 1.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-react-native-media-images@0.13.27
|
||||
|
||||
## 1.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-expo-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.117",
|
||||
"version": "1.0.119",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# chat-rn-expo
|
||||
|
||||
## 1.0.106
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 1.0.105
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 1.0.104
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn-expo",
|
||||
"version": "1.0.104",
|
||||
"version": "1.0.106",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- Updated dependencies [422dbc4]
|
||||
- cojson@0.13.28
|
||||
- cojson-transport-ws@0.13.28
|
||||
- jazz-react-native@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 1.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-transport-ws@0.13.27
|
||||
- jazz-react-native@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 1.0.112
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.112",
|
||||
"version": "1.0.114",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-vue@0.13.28
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-vue@0.13.27
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.95",
|
||||
"version": "0.0.97",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.195
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.28
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.194
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.27
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.193
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.193",
|
||||
"version": "0.0.195",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-react-auth-clerk@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-react-auth-clerk@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.92",
|
||||
"version": "0.0.94",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-inspector-element@0.13.28
|
||||
|
||||
## 0.0.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-inspector-element@0.13.27
|
||||
|
||||
## 0.0.76
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.76",
|
||||
"version": "0.0.78",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-tailwind-demo-auth-starter
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.28
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.27
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.34",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# form
|
||||
|
||||
## 0.1.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.1.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.1.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.33",
|
||||
"version": "0.1.35",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.91",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.145
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- Updated dependencies [422dbc4]
|
||||
- cojson@0.13.28
|
||||
- cojson-transport-ws@0.13.28
|
||||
- jazz-inspector@0.13.28
|
||||
|
||||
## 0.0.144
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-transport-ws@0.13.27
|
||||
- jazz-inspector@0.13.27
|
||||
|
||||
## 0.0.143
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.143",
|
||||
"version": "0.0.145",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# multi-cursors
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multi-cursors",
|
||||
"private": true,
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.87",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# multiauth
|
||||
|
||||
## 0.0.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-react-auth-clerk@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-react-auth-clerk@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.33",
|
||||
"version": "0.0.35",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.28
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.27
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.114",
|
||||
"version": "0.0.116",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# organization
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.87",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.80",
|
||||
"version": "0.0.82",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.90",
|
||||
"version": "0.0.92",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.87",
|
||||
"version": "0.0.89",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.112
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.111
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.111",
|
||||
"version": "0.0.113",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.211
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.210
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.209
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.209",
|
||||
"version": "0.0.211",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.91",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# richtext-tiptap
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-richtext-tiptap@0.1.4
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-richtext-tiptap@0.1.3
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext-tiptap",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# richtext
|
||||
|
||||
## 0.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-richtext-prosemirror@0.1.15
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-richtext-prosemirror@0.1.14
|
||||
|
||||
## 0.0.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext",
|
||||
"private": true,
|
||||
"version": "0.0.79",
|
||||
"version": "0.0.81",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
- jazz-vue@0.13.28
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
- jazz-vue@0.13.27
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.95",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.210
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.209
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.208
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.208",
|
||||
"version": "0.0.210",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.28
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.27
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.87",
|
||||
"version": "0.0.89",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -8,6 +8,8 @@ The main detail to understand when using Jazz server-side is that Server Workers
|
||||
|
||||
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
|
||||
|
||||
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
|
||||
|
||||
## Generating credentials
|
||||
|
||||
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
|
||||
|
||||
@@ -20,8 +20,13 @@ These root references are modeled explicitly in your schema, distinguishing betw
|
||||
Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, CoList } from "jazz-tools";
|
||||
class Chat extends CoMap {};
|
||||
class ListOfChats extends CoList.Of(Chat) {};
|
||||
class ListOfAccounts extends CoList.Of(Account) {};
|
||||
import "jazz-react";
|
||||
// ---cut---
|
||||
import { Account, CoMap } from "jazz-tools";
|
||||
|
||||
export class MyAppAccount extends Account {
|
||||
@@ -50,9 +55,11 @@ that is set up for you based on the username the `AuthMethod` provides on accoun
|
||||
Their pre-defined schemas roughly look like this:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
// ...somehwere in jazz-tools itself...
|
||||
```ts twoslash
|
||||
// @noErrors: 2416
|
||||
import { co, Group, CoMap } from "jazz-tools";
|
||||
// ---cut---
|
||||
// ...somewhere in jazz-tools itself...
|
||||
export class Account extends Group {
|
||||
profile = co.ref(Profile);
|
||||
}
|
||||
@@ -68,8 +75,10 @@ If you want to keep the default `Profile` schema, but customise your account's p
|
||||
(You don't have to explicitly re-define the `profile` field, but it makes it more readable that the Account contains both `profile` and `root`)
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { co, CoMap } from "jazz-tools";
|
||||
class MyAppRoot extends CoMap {};
|
||||
// ---cut---
|
||||
import { Account, Profile } from "jazz-tools";
|
||||
|
||||
export class MyAppAccount extends Account {
|
||||
@@ -78,11 +87,17 @@ export class MyAppAccount extends Account {
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
If you want to extend the `profile` to contain additional fields (such as an avatar `ImageDefinition`), you can declare your own profile schema class that extends `Profile`:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import "jazz-react";
|
||||
import { co, CoMap, CoList } from "jazz-tools";
|
||||
class Chat extends CoMap {};
|
||||
class ListOfChats extends CoList.Of(Chat) {};
|
||||
class ListOfAccounts extends CoList.Of(Account) {};
|
||||
// ---cut---
|
||||
import { Account, Profile, ImageDefinition } from "jazz-tools"; // [!code ++]
|
||||
|
||||
export class MyAppAccount extends Account {
|
||||
@@ -102,9 +117,9 @@ export class MyAppProfile extends Profile { // [!code ++:4]
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -115,23 +130,65 @@ declare module "jazz-react" {
|
||||
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
```tsx twoslash
|
||||
import * as React from "react";
|
||||
import { Account, co, CoMap, Profile, CoList } from "jazz-tools";
|
||||
|
||||
class Chat extends CoMap {};
|
||||
class ListOfChats extends CoList.Of(co.ref(Chat)) {};
|
||||
class ListOfAccounts extends CoList.Of(co.ref(Account)) {};
|
||||
|
||||
class MyAppRoot extends CoMap {
|
||||
myChats = co.ref(ListOfChats);
|
||||
myContacts = co.ref(ListOfAccounts);
|
||||
};
|
||||
class MyAppProfile extends Profile {};
|
||||
|
||||
class MyAppAccount extends Account {
|
||||
root = co.ref(MyAppRoot);
|
||||
profile = co.ref(MyAppProfile);
|
||||
};
|
||||
|
||||
declare module "jazz-react" {
|
||||
interface Register {
|
||||
Account: MyAppAccount;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatPreview extends React.Component<{ chat: Chat }> {};
|
||||
class ContactPreview extends React.Component<{ contact: Account }> {};
|
||||
// ---cut---
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
function DashboardPageComponent() {
|
||||
const { me } = useAccount({ profile: {}, root: { myChats: {}, myContacts: {}}});
|
||||
const { me } = useAccount({ resolve: {
|
||||
profile: true,
|
||||
root: {
|
||||
myChats: { $each: true },
|
||||
myContacts: { $each: true }
|
||||
}
|
||||
}});
|
||||
|
||||
return <div>
|
||||
<h1>Dashboard</h1>
|
||||
{me ? <div>
|
||||
<p>Logged in as {me.profile.name}</p>
|
||||
<h2>My chats</h2>
|
||||
{me.root.myChats.map((chat) => <ChatPreview key={chat.id} chat={chat} />)}
|
||||
<h2>My contacts</h2>
|
||||
{me.root.myContacts.map((contact) => <ContactPreview key={contact.id} contact={contact} />)}
|
||||
</div> : "Loading..."}
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
{me ? (
|
||||
<div>
|
||||
<p>Logged in as {me.profile.name}</p>
|
||||
<h2>My chats</h2>
|
||||
{me.root.myChats.map((chat) => (
|
||||
<ChatPreview key={chat.id} chat={chat} />
|
||||
))}
|
||||
<h2>My contacts</h2>
|
||||
{me.root.myContacts.map((contact) => (
|
||||
<ContactPreview key={contact.id} contact={contact} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
"Loading..."
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
@@ -156,8 +213,21 @@ Jazz waits for the migration to finish before passing the account to your app's
|
||||
### Initialising user data after account creation
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Account, co, Group, CoList, CoMap, Profile } from "jazz-tools";
|
||||
class Chat extends CoMap {};
|
||||
class Bookmark extends CoMap {};
|
||||
|
||||
class ListOfChats extends CoList.Of(co.ref(Chat)) {};
|
||||
class ListOfAccounts extends CoList.Of(co.ref(Account)) {};
|
||||
class ListOfBookmarks extends CoList.Of(co.ref(Bookmark)) {};
|
||||
|
||||
class MyAppRoot extends CoMap {};
|
||||
class MyAppProfile extends Profile {
|
||||
name = co.string;
|
||||
bookmarks = co.ref(ListOfBookmarks);
|
||||
};
|
||||
// ---cut---
|
||||
export class MyAppAccount extends Account {
|
||||
root = co.ref(MyAppRoot);
|
||||
profile = co.ref(MyAppProfile);
|
||||
@@ -181,7 +251,7 @@ export class MyAppAccount extends Account {
|
||||
profileGroup.addMember("everyone", "reader");
|
||||
|
||||
this.profile = MyAppProfile.create({
|
||||
name: creationProps?.name,
|
||||
name: creationProps?.name ?? "New user",
|
||||
bookmarks: ListOfBookmarks.create([], profileGroup),
|
||||
}, profileGroup);
|
||||
}
|
||||
@@ -200,8 +270,22 @@ To do deeply nested migrations, you might need to use the asynchronous `ensureLo
|
||||
Now let's say we want to add a `myBookmarks` field to the `root` schema:
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { Account, co, Group, CoList, CoMap, Profile } from "jazz-tools";
|
||||
class Chat extends CoMap {};
|
||||
class Bookmark extends CoMap {};
|
||||
|
||||
class ListOfChats extends CoList.Of(co.ref(Chat)) {};
|
||||
class ListOfAccounts extends CoList.Of(co.ref(Account)) {};
|
||||
class ListOfBookmarks extends CoList.Of(co.ref(Bookmark)) {};
|
||||
|
||||
class MyAppRoot extends CoMap {
|
||||
myChats = co.ref(ListOfChats);
|
||||
myContacts = co.ref(ListOfAccounts);
|
||||
myBookmarks = co.optional.ref(ListOfBookmarks);
|
||||
};
|
||||
|
||||
// ---cut---
|
||||
export class MyAppAccount extends Account {
|
||||
root = co.ref(MyAppRoot);
|
||||
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- cojson-storage@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-storage@0.13.27
|
||||
|
||||
## 0.13.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.13.25",
|
||||
"version": "0.13.28",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- cojson-storage@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-storage@0.13.27
|
||||
|
||||
## 0.13.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.13.25",
|
||||
"version": "0.13.28",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.13.25",
|
||||
"cojson": "workspace:0.13.28",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { LocalNode } from "cojson";
|
||||
import { LocalNode, cojsonInternals } from "cojson";
|
||||
import { SyncManager } from "cojson-storage";
|
||||
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { expect, onTestFinished, test, vi } from "vitest";
|
||||
@@ -403,3 +403,92 @@ test("should recover from data loss", async () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("should recover missing dependencies from storage", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const account = LocalNode.internalCreateAccount({
|
||||
crypto: Crypto,
|
||||
});
|
||||
const node1 = account.core.node;
|
||||
|
||||
const serverNode = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const [serverPeer, clientPeer] = cojsonInternals.connectedPeers(
|
||||
node1.agentSecret,
|
||||
serverNode.agentSecret,
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
|
||||
node1.syncManager.addPeer(serverPeer);
|
||||
serverNode.syncManager.addPeer(clientPeer);
|
||||
|
||||
const handleSyncMessage = SyncManager.prototype.handleSyncMessage;
|
||||
|
||||
const mock = vi
|
||||
.spyOn(SyncManager.prototype, "handleSyncMessage")
|
||||
.mockImplementation(function (this: SyncManager, msg) {
|
||||
if (
|
||||
msg.action === "content" &&
|
||||
[group.core.id, account.core.id].includes(msg.id)
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return handleSyncMessage.call(this, msg);
|
||||
});
|
||||
|
||||
const { peer, dbPath } = await createSQLiteStorage();
|
||||
|
||||
node1.syncManager.addPeer(peer);
|
||||
|
||||
const group = node1.createGroup();
|
||||
group.addMember("everyone", "writer");
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("0", 0);
|
||||
|
||||
mock.mockReset();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
const node2 = new LocalNode(
|
||||
Crypto.newRandomAgentSecret(),
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const [serverPeer2, clientPeer2] = cojsonInternals.connectedPeers(
|
||||
node1.agentSecret,
|
||||
serverNode.agentSecret,
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
|
||||
node2.syncManager.addPeer(serverPeer2);
|
||||
serverNode.syncManager.addPeer(clientPeer2);
|
||||
|
||||
const { peer: peer2 } = await createSQLiteStorage(dbPath);
|
||||
|
||||
node2.syncManager.addPeer(peer2);
|
||||
|
||||
const map2 = await node2.load(map.id);
|
||||
|
||||
if (map2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(map2.toJSON()).toEqual({
|
||||
"0": 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
|
||||
## 0.13.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.13.25",
|
||||
"version": "0.13.28",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 422dbc4: Add waitUntilConnected and subscribe APIs on WebSocketPeerWithReconnection
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
|
||||
## 0.13.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.13.25",
|
||||
"version": "0.13.28",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { type Peer, logger } from "cojson";
|
||||
import { createWebSocketPeer } from "./createWebSocketPeer.js";
|
||||
import type { AnyWebSocketConstructor } from "./types.js";
|
||||
|
||||
export class WebSocketPeerWithReconnection {
|
||||
private peer: string;
|
||||
private reconnectionTimeout: number;
|
||||
private addPeer: (peer: Peer) => void;
|
||||
private removePeer: (peer: Peer) => void;
|
||||
private WebSocketConstructor: AnyWebSocketConstructor;
|
||||
private pingTimeout: number;
|
||||
|
||||
constructor(opts: {
|
||||
peer: string;
|
||||
reconnectionTimeout: number | undefined;
|
||||
addPeer: (peer: Peer) => void;
|
||||
removePeer: (peer: Peer) => void;
|
||||
WebSocketConstructor?: AnyWebSocketConstructor;
|
||||
pingTimeout?: number;
|
||||
}) {
|
||||
this.peer = opts.peer;
|
||||
this.reconnectionTimeout = opts.reconnectionTimeout || 500;
|
||||
this.addPeer = opts.addPeer;
|
||||
this.removePeer = opts.removePeer;
|
||||
this.WebSocketConstructor = opts.WebSocketConstructor || WebSocket;
|
||||
this.pingTimeout = opts.pingTimeout || 10_000;
|
||||
}
|
||||
|
||||
state: "disabled" | "enabled" = "disabled";
|
||||
enabled = false;
|
||||
closed = true;
|
||||
|
||||
currentPeer: Peer | undefined = undefined;
|
||||
unsubscribeNetworkChange: (() => void) | undefined = undefined;
|
||||
private unsubscribeNetworkChange: (() => void) | undefined = undefined;
|
||||
|
||||
// Basic implementation for environments that don't support network change events (e.g. Node.js)
|
||||
// Needs to be extended to handle platform specific APIs
|
||||
@@ -30,7 +39,7 @@ export class WebSocketPeerWithReconnection {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
waitForOnline(timeout: number) {
|
||||
private waitForOnline(timeout: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsubscribeNetworkChange = this.onNetworkChange((connected) => {
|
||||
if (connected) {
|
||||
@@ -40,7 +49,7 @@ export class WebSocketPeerWithReconnection {
|
||||
|
||||
function handleTimeoutOrOnline() {
|
||||
clearTimeout(timer);
|
||||
unsubscribeNetworkChange();
|
||||
unsubscribeNetworkChange?.();
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -50,11 +59,38 @@ export class WebSocketPeerWithReconnection {
|
||||
|
||||
reconnectionAttempts = 0;
|
||||
|
||||
onConnectionChangeListeners = new Set<(connected: boolean) => void>();
|
||||
|
||||
waitUntilConnected = async () => {
|
||||
if (this.closed) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const listener = (connected: boolean) => {
|
||||
if (connected) {
|
||||
resolve();
|
||||
this.onConnectionChangeListeners.delete(listener);
|
||||
}
|
||||
};
|
||||
|
||||
this.onConnectionChangeListeners.add(listener);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
subscribe = (listener: (connected: boolean) => void) => {
|
||||
this.onConnectionChangeListeners.add(listener);
|
||||
listener(!this.closed);
|
||||
};
|
||||
|
||||
unsubscribe = (listener: (connected: boolean) => void) => {
|
||||
this.onConnectionChangeListeners.delete(listener);
|
||||
};
|
||||
|
||||
startConnection = async () => {
|
||||
if (this.state !== "enabled") return;
|
||||
if (!this.enabled) return;
|
||||
|
||||
if (this.currentPeer) {
|
||||
this.removePeer(this.currentPeer);
|
||||
this.currentPeer.outgoing.close();
|
||||
|
||||
this.reconnectionAttempts++;
|
||||
|
||||
@@ -67,14 +103,25 @@ export class WebSocketPeerWithReconnection {
|
||||
await this.waitForOnline(timeout);
|
||||
}
|
||||
|
||||
if (this.state !== "enabled") return;
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.currentPeer = createWebSocketPeer({
|
||||
websocket: new WebSocket(this.peer),
|
||||
websocket: new this.WebSocketConstructor(this.peer),
|
||||
pingTimeout: this.pingTimeout,
|
||||
id: this.peer,
|
||||
role: "server",
|
||||
onClose: this.startConnection,
|
||||
onClose: () => {
|
||||
this.closed = true;
|
||||
for (const listener of this.onConnectionChangeListeners) {
|
||||
listener(false);
|
||||
}
|
||||
this.startConnection();
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.closed = false;
|
||||
for (const listener of this.onConnectionChangeListeners) {
|
||||
listener(true);
|
||||
}
|
||||
logger.debug("Websocket connection successful");
|
||||
|
||||
this.reconnectionAttempts = 0;
|
||||
@@ -85,16 +132,16 @@ export class WebSocketPeerWithReconnection {
|
||||
};
|
||||
|
||||
enable = () => {
|
||||
if (this.state === "enabled") return;
|
||||
if (this.enabled) return;
|
||||
|
||||
this.state = "enabled";
|
||||
this.enabled = true;
|
||||
this.startConnection();
|
||||
};
|
||||
|
||||
disable = () => {
|
||||
if (this.state === "disabled") return;
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.state = "disabled";
|
||||
this.enabled = false;
|
||||
|
||||
this.reconnectionAttempts = 0;
|
||||
this.unsubscribeNetworkChange?.();
|
||||
@@ -102,6 +149,7 @@ export class WebSocketPeerWithReconnection {
|
||||
|
||||
if (this.currentPeer) {
|
||||
this.removePeer(this.currentPeer);
|
||||
this.currentPeer.outgoing.close();
|
||||
this.currentPeer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,4 +140,245 @@ describe("WebSocketPeerWithReconnection", () => {
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
describe("waitUntilConnected", () => {
|
||||
test("should wait until connected before resolving", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
// Start waiting for connection before enabling
|
||||
const waitPromise = peer.waitUntilConnected();
|
||||
|
||||
// Enable the peer after a short delay
|
||||
setTimeout(() => peer.enable(), 100);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitPromise;
|
||||
|
||||
expect(addPeer).toHaveBeenCalledTimes(1);
|
||||
expect(peer.closed).toBe(false);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
test("should resolve immediately if already connected", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
// Enable the peer first
|
||||
peer.enable();
|
||||
|
||||
// Wait for initial connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Now wait for connection again
|
||||
const waitPromise = peer.waitUntilConnected();
|
||||
|
||||
// Should resolve immediately since we're already connected
|
||||
await waitPromise;
|
||||
|
||||
expect(addPeer).toHaveBeenCalledTimes(1);
|
||||
expect(peer.closed).toBe(false);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
test("should work when connection is lost and regained", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
peer.enable();
|
||||
|
||||
// Wait for initial connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Close server to simulate connection loss
|
||||
server.close();
|
||||
|
||||
// Wait for disconnect to be detected
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Start server again
|
||||
server = await startSyncServer(server.port);
|
||||
|
||||
// Wait for the waitUntilConnected promise to resolve
|
||||
await peer.waitUntilConnected();
|
||||
|
||||
// Verify that we have a new connection
|
||||
expect(addPeer).toHaveBeenCalledTimes(3); // Once for initial, once for reconnection
|
||||
expect(peer.closed).toBe(false);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribe", () => {
|
||||
test("should notify subscribers of initial connection state", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
const listener = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
// Subscribe before enabling
|
||||
peer.subscribe(listener);
|
||||
|
||||
// Initial state should be disconnected
|
||||
expect(listener).toHaveBeenCalledWith(false);
|
||||
|
||||
// Enable the peer
|
||||
peer.enable();
|
||||
|
||||
// Wait for connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Should notify of connected state
|
||||
expect(listener).toHaveBeenCalledWith(true);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
test("should notify subscribers of connection state changes", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
const listener = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
peer.enable();
|
||||
peer.subscribe(listener);
|
||||
|
||||
// Wait for initial connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Should notify of connected state
|
||||
expect(listener).toHaveBeenCalledWith(true);
|
||||
|
||||
// Close server to simulate disconnect
|
||||
server.close();
|
||||
|
||||
// Wait for disconnect to be detected
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Should notify of disconnected state
|
||||
expect(listener).toHaveBeenCalledWith(false);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
test("should not notify unsubscribed listeners", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
const listener1 = vi.fn();
|
||||
const listener2 = vi.fn();
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
peer.enable();
|
||||
peer.subscribe(listener1);
|
||||
peer.subscribe(listener2);
|
||||
|
||||
// Wait for initial connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Both listeners should be notified of connection
|
||||
expect(listener1).toHaveBeenCalledWith(true);
|
||||
expect(listener2).toHaveBeenCalledWith(true);
|
||||
|
||||
// Unsubscribe listener1
|
||||
peer.unsubscribe(listener1);
|
||||
|
||||
listener1.mockClear();
|
||||
listener2.mockClear();
|
||||
|
||||
// Close server to simulate disconnect
|
||||
server.close();
|
||||
|
||||
// Wait for disconnect to be detected
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// Only listener2 should be notified of disconnect
|
||||
expect(listener1).not.toHaveBeenCalled();
|
||||
expect(listener2).toHaveBeenCalledWith(false);
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
|
||||
test("should handle multiple subscribers correctly", async () => {
|
||||
const addPeer = vi.fn();
|
||||
const removePeer = vi.fn();
|
||||
const listeners = Array.from({ length: 3 }, () => vi.fn());
|
||||
|
||||
const peer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServerUrl,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer,
|
||||
removePeer,
|
||||
});
|
||||
|
||||
peer.enable();
|
||||
|
||||
// Subscribe all listeners
|
||||
for (const listener of listeners) {
|
||||
peer.subscribe(listener);
|
||||
}
|
||||
|
||||
// Wait for initial connection
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// All listeners should be notified of connection
|
||||
for (const listener of listeners) {
|
||||
expect(listener).toHaveBeenCalledWith(true);
|
||||
}
|
||||
|
||||
// Close server to simulate disconnect
|
||||
server.close();
|
||||
|
||||
// Wait for disconnect to be detected
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// All listeners should be notified of disconnect
|
||||
for (const listener of listeners) {
|
||||
expect(listener).toHaveBeenCalledWith(false);
|
||||
}
|
||||
|
||||
peer.disable();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# cojson
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e7ccb2c: Recover missing dependencies when getting new content
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6357052: Allow accounts to self-remove from groups
|
||||
|
||||
## 0.13.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.13.25",
|
||||
"version": "0.13.28",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -109,7 +109,7 @@ export class CoValueCore {
|
||||
private readonly _decryptionCache: {
|
||||
[key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
|
||||
} = {};
|
||||
private _cachedDependentOn?: RawCoID[];
|
||||
private _cachedDependentOn?: Set<RawCoID>;
|
||||
private counter: UpDownCounter;
|
||||
|
||||
private constructor(
|
||||
@@ -897,39 +897,57 @@ export class CoValueCore {
|
||||
];
|
||||
}
|
||||
|
||||
getDependedOnCoValues(): RawCoID[] {
|
||||
getDependedOnCoValues(): Set<RawCoID> {
|
||||
if (this._cachedDependentOn) {
|
||||
return this._cachedDependentOn;
|
||||
} else {
|
||||
const dependentOn = this.getDependedOnCoValuesUncached();
|
||||
if (!this.verified) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const dependentOn = this.getDependedOnCoValuesFromHeaderAndSessions(
|
||||
this.verified.header,
|
||||
this.verified.sessions.keys(),
|
||||
);
|
||||
this._cachedDependentOn = dependentOn;
|
||||
return dependentOn;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getDependedOnCoValuesUncached(): RawCoID[] {
|
||||
if (!this.verified) {
|
||||
return [];
|
||||
getDependedOnCoValuesFromHeaderAndSessions(
|
||||
header: CoValueHeader,
|
||||
sessions: Iterable<SessionID>,
|
||||
): Set<RawCoID> {
|
||||
const deps = new Set<RawCoID>();
|
||||
|
||||
for (const session of sessions) {
|
||||
const accountId = accountOrAgentIDfromSessionID(session);
|
||||
|
||||
if (isAccountID(accountId) && accountId !== this.id) {
|
||||
deps.add(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
return this.verified.header.ruleset.type === "group"
|
||||
? getGroupDependentKeyList(expectGroup(this.getCurrentContent()).keys())
|
||||
: this.verified.header.ruleset.type === "ownedByGroup"
|
||||
? [
|
||||
this.verified.header.ruleset.group,
|
||||
...new Set(
|
||||
[...this.verified.sessions.keys()]
|
||||
.map((sessionID) =>
|
||||
accountOrAgentIDfromSessionID(sessionID as SessionID),
|
||||
)
|
||||
.filter(
|
||||
(session): session is RawAccountID =>
|
||||
isAccountID(session) && session !== this.id,
|
||||
),
|
||||
),
|
||||
]
|
||||
: [];
|
||||
if (header.ruleset.type === "group") {
|
||||
if (isAccountID(header.ruleset.initialAdmin)) {
|
||||
deps.add(header.ruleset.initialAdmin);
|
||||
}
|
||||
|
||||
if (this.verified) {
|
||||
for (const id of getGroupDependentKeyList(
|
||||
expectGroup(this.getCurrentContent()).keys(),
|
||||
)) {
|
||||
deps.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (header.ruleset.type === "ownedByGroup") {
|
||||
deps.add(header.ruleset.group);
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
waitForSync(options?: {
|
||||
@@ -943,7 +961,11 @@ export class CoValueCore {
|
||||
return;
|
||||
}
|
||||
|
||||
const peersToActuallyLoadFrom = [];
|
||||
const peersToActuallyLoadFrom = {
|
||||
storage: [] as PeerState[],
|
||||
server: [] as PeerState[],
|
||||
};
|
||||
|
||||
for (const peer of peers) {
|
||||
const currentState = this.peers.get(peer.id);
|
||||
|
||||
@@ -959,78 +981,87 @@ export class CoValueCore {
|
||||
}
|
||||
|
||||
if (currentState?.type === "unavailable") {
|
||||
if (peer.shouldRetryUnavailableCoValues()) {
|
||||
if (peer.role === "server") {
|
||||
peersToActuallyLoadFrom.server.push(peer);
|
||||
this.markPending(peer.id);
|
||||
peersToActuallyLoadFrom.push(peer);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentState || currentState?.type === "unknown") {
|
||||
if (peer.role === "storage") {
|
||||
peersToActuallyLoadFrom.storage.push(peer);
|
||||
} else {
|
||||
peersToActuallyLoadFrom.server.push(peer);
|
||||
}
|
||||
|
||||
this.markPending(peer.id);
|
||||
peersToActuallyLoadFrom.push(peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const peer of peersToActuallyLoadFrom) {
|
||||
if (peer.closed) {
|
||||
this.markNotFoundInPeer(peer.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
peer.pushOutgoingMessage({
|
||||
action: "load",
|
||||
...this.knownState(),
|
||||
});
|
||||
peer.trackLoadRequestSent(this.id);
|
||||
|
||||
/**
|
||||
* Use a very long timeout for storage peers, because under pressure
|
||||
* they may take a long time to consume the messages queue
|
||||
*
|
||||
* TODO: Track errors on storage and do not rely on timeout
|
||||
*/
|
||||
const timeoutDuration =
|
||||
peer.role === "storage"
|
||||
? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
|
||||
: CO_VALUE_LOADING_CONFIG.TIMEOUT;
|
||||
|
||||
const waitingForPeer = new Promise<void>((resolve) => {
|
||||
const markNotFound = () => {
|
||||
if (this.peers.get(peer.id)?.type === "pending") {
|
||||
logger.warn("Timeout waiting for peer to load coValue", {
|
||||
id: this.id,
|
||||
peerID: peer.id,
|
||||
});
|
||||
this.markNotFoundInPeer(peer.id);
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(markNotFound, timeoutDuration);
|
||||
const removeCloseListener = peer.addCloseListener(markNotFound);
|
||||
|
||||
const listener = (state: CoValueCore) => {
|
||||
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"
|
||||
) {
|
||||
this.listeners.delete(listener);
|
||||
removeCloseListener();
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.listeners.add(listener);
|
||||
listener(this);
|
||||
});
|
||||
|
||||
await waitingForPeer;
|
||||
// Load from storage peers first, then from server peers
|
||||
if (peersToActuallyLoadFrom.storage.length > 0) {
|
||||
await Promise.all(
|
||||
peersToActuallyLoadFrom.storage.map((peer) =>
|
||||
this.internalLoadFromPeer(peer),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (peersToActuallyLoadFrom.server.length > 0) {
|
||||
await Promise.all(
|
||||
peersToActuallyLoadFrom.server.map((peer) =>
|
||||
this.internalLoadFromPeer(peer),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internalLoadFromPeer(peer: PeerState) {
|
||||
if (peer.closed) {
|
||||
this.markNotFoundInPeer(peer.id);
|
||||
return;
|
||||
}
|
||||
|
||||
peer.pushOutgoingMessage({
|
||||
action: "load",
|
||||
...this.knownState(),
|
||||
});
|
||||
peer.trackLoadRequestSent(this.id);
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const markNotFound = () => {
|
||||
if (this.peers.get(peer.id)?.type === "pending") {
|
||||
logger.warn("Timeout waiting for peer to load coValue", {
|
||||
id: this.id,
|
||||
peerID: peer.id,
|
||||
});
|
||||
this.markNotFoundInPeer(peer.id);
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
|
||||
const removeCloseListener = peer.addCloseListener(markNotFound);
|
||||
|
||||
const listener = (state: CoValueCore) => {
|
||||
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"
|
||||
) {
|
||||
this.listeners.delete(listener);
|
||||
removeCloseListener();
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.listeners.add(listener);
|
||||
listener(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -773,7 +773,10 @@ export class RawGroup<
|
||||
) {
|
||||
const memberKey = typeof account === "string" ? account : account.id;
|
||||
|
||||
this.rotateReadKey(memberKey);
|
||||
if (this.myRole() === "admin") {
|
||||
this.rotateReadKey(memberKey);
|
||||
}
|
||||
|
||||
this.set(memberKey, "revoked", "trusting");
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,12 @@ export class LocalNode {
|
||||
this.coValues.delete(id);
|
||||
}
|
||||
|
||||
getCurrentAccountOrAgentID(): RawAccountID | AgentID {
|
||||
return accountOrAgentIDfromSessionID(this.currentSessionID);
|
||||
}
|
||||
|
||||
getCurrentAgent(): ControlledAccountOrAgent {
|
||||
const accountOrAgent = accountOrAgentIDfromSessionID(this.currentSessionID);
|
||||
const accountOrAgent = this.getCurrentAccountOrAgentID();
|
||||
if (isAgentID(accountOrAgent)) {
|
||||
return new ControlledAgent(this.agentSecret, this.crypto);
|
||||
}
|
||||
@@ -118,7 +122,7 @@ export class LocalNode {
|
||||
}
|
||||
|
||||
expectCurrentAccountID(reason: string): RawAccountID {
|
||||
const accountOrAgent = accountOrAgentIDfromSessionID(this.currentSessionID);
|
||||
const accountOrAgent = this.getCurrentAccountOrAgentID();
|
||||
if (isAgentID(accountOrAgent)) {
|
||||
throw new Error(
|
||||
"Current account is an agent, but expected an account: " + reason,
|
||||
|
||||
@@ -452,7 +452,12 @@ function determineValidTransactionsForGroup(
|
||||
change.key === transactor &&
|
||||
change.value === "admin";
|
||||
|
||||
if (!isFirstSelfAppointment) {
|
||||
const currentAccountId = coValue.node.getCurrentAccountOrAgentID();
|
||||
|
||||
const isSelfRevoke =
|
||||
currentAccountId === change.key && change.value === "revoked";
|
||||
|
||||
if (!isFirstSelfAppointment && !isSelfRevoke) {
|
||||
if (memberState[transactor] === "admin") {
|
||||
if (
|
||||
memberState[affectedMember] === "admin" &&
|
||||
|
||||
@@ -11,6 +11,8 @@ import { RawCoID, SessionID } from "./ids.js";
|
||||
import { LocalNode } from "./localNode.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { CoValuePriority } from "./priority.js";
|
||||
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
||||
import { isAccountID } from "./typeUtils/isAccountID.js";
|
||||
|
||||
export type CoValueKnownState = {
|
||||
id: RawCoID;
|
||||
@@ -211,9 +213,9 @@ export class SyncManager {
|
||||
return;
|
||||
}
|
||||
|
||||
coValue
|
||||
.getDependedOnCoValues()
|
||||
.map((id) => this.sendNewContentIncludingDependencies(id, peer));
|
||||
for (const dependency of coValue.getDependedOnCoValues()) {
|
||||
this.sendNewContentIncludingDependencies(dependency, peer);
|
||||
}
|
||||
|
||||
const newContentPieces = coValue.verified.newContentSince(
|
||||
peer.optimisticKnownStates.get(id),
|
||||
@@ -298,7 +300,7 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
async addPeer(peer: Peer) {
|
||||
addPeer(peer: Peer) {
|
||||
const prevPeer = this.peers[peer.id];
|
||||
|
||||
if (prevPeer && !prevPeer.closed) {
|
||||
@@ -379,65 +381,17 @@ export class SyncManager {
|
||||
peer.setKnownState(msg.id, knownStateIn(msg));
|
||||
const coValue = this.local.getCoValue(msg.id);
|
||||
|
||||
if (
|
||||
coValue.loadingState === "unknown" ||
|
||||
coValue.loadingState === "unavailable"
|
||||
) {
|
||||
const eligiblePeers = this.getServerAndStoragePeers(peer.id);
|
||||
|
||||
if (eligiblePeers.length === 0) {
|
||||
// We don't have any eligible peers to load the coValue from
|
||||
// so we send a known state back to the sender to let it know
|
||||
// that the coValue is unavailable
|
||||
peer.trackToldKnownState(msg.id);
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
// Syncronously updates the state loading is possible
|
||||
coValue
|
||||
.loadFromPeers(this.getServerAndStoragePeers(peer.id))
|
||||
.catch((e) => {
|
||||
logger.error("Error loading coValue in handleLoad", { err: e });
|
||||
});
|
||||
}
|
||||
if (coValue.isAvailable()) {
|
||||
this.sendNewContentIncludingDependencies(msg.id, peer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coValue.loadingState === "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
|
||||
// content from a client, but we are a server with our own upstream server(s)
|
||||
coValue
|
||||
.waitForAvailableOrUnavailable()
|
||||
.then(async (value) => {
|
||||
if (!value.isAvailable()) {
|
||||
peer.trackToldKnownState(msg.id);
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
const eligiblePeers = this.getServerAndStoragePeers(peer.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendNewContentIncludingDependencies(msg.id, peer);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error loading coValue in handleLoad loading state", {
|
||||
err: e,
|
||||
});
|
||||
});
|
||||
} else if (coValue.isAvailable()) {
|
||||
this.sendNewContentIncludingDependencies(msg.id, peer);
|
||||
} else {
|
||||
if (eligiblePeers.length === 0) {
|
||||
// We don't have any eligible peers to load the coValue from
|
||||
// so we send a known state back to the sender to let it know
|
||||
// that the coValue is unavailable
|
||||
peer.trackToldKnownState(msg.id);
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
@@ -445,9 +399,41 @@ export class SyncManager {
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
coValue.loadFromPeers(eligiblePeers).catch((e) => {
|
||||
logger.error("Error loading coValue in handleLoad", { err: e });
|
||||
});
|
||||
|
||||
// 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
|
||||
// content from a client, but we are a server with our own upstream server(s)
|
||||
coValue
|
||||
.waitForAvailableOrUnavailable()
|
||||
.then((value) => {
|
||||
if (!value.isAvailable()) {
|
||||
peer.trackToldKnownState(msg.id);
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendNewContentIncludingDependencies(msg.id, peer);
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error loading coValue in handleLoad loading state", {
|
||||
err: e,
|
||||
});
|
||||
});
|
||||
}
|
||||
handleKnownState(msg: KnownStateMessage, peer: PeerState) {
|
||||
const coValue = this.local.getCoValue(msg.id);
|
||||
|
||||
@@ -494,6 +480,52 @@ export class SyncManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let dependencyMissing = false;
|
||||
const sessionIDs = Object.keys(msg.new) as SessionID[];
|
||||
for (const dependency of coValue.getDependedOnCoValuesFromHeaderAndSessions(
|
||||
msg.header,
|
||||
sessionIDs,
|
||||
)) {
|
||||
const dependencyCoValue = this.local.getCoValue(dependency);
|
||||
|
||||
if (!dependencyCoValue.isAvailable()) {
|
||||
if (peer.role !== "storage") {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "load",
|
||||
id: dependency,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
}
|
||||
|
||||
dependencyMissing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencyMissing) {
|
||||
if (peer.role !== "storage") {
|
||||
/**
|
||||
* If we have missing dependencies, we send a known state message to the peer
|
||||
* to let it know that we need a correction update.
|
||||
*
|
||||
* Sync-wise is sub-optimal, but it gives us correctness until
|
||||
* https://github.com/garden-co/jazz/issues/1917 is implemented.
|
||||
*/
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
isCorrection: true,
|
||||
id: msg.id,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
} else {
|
||||
/** Cases of broken deps from storage are recovered by falling back to the server peers */
|
||||
coValue.loadFromPeers(this.getServerAndStoragePeers(peer.id));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
peer.updateHeader(msg.id, true);
|
||||
coValue.markAvailable(msg.header, peer.id);
|
||||
}
|
||||
@@ -528,6 +560,18 @@ export class SyncManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
const accountId = accountOrAgentIDfromSessionID(sessionID);
|
||||
|
||||
if (isAccountID(accountId)) {
|
||||
const account = this.local.getCoValue(accountId);
|
||||
|
||||
if (!account.isAvailable()) {
|
||||
account.loadFromPeers([peer]);
|
||||
invalidStateAssumed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const result = coValue.tryAddTransactions(
|
||||
sessionID,
|
||||
newTransactions,
|
||||
@@ -607,6 +651,8 @@ export class SyncManager {
|
||||
// Check if there is a inflight load operation and we
|
||||
// are waiting for other peers to send the load request
|
||||
if (state === "unknown" || state === undefined) {
|
||||
// Sending a load message to the peer to get to know how much content is missing
|
||||
// before sending the new content
|
||||
this.trySendToPeer(peer, {
|
||||
action: "load",
|
||||
...coValue.knownState(),
|
||||
@@ -645,7 +691,7 @@ export class SyncManager {
|
||||
this.requestedSyncs.add(coValue.id);
|
||||
}
|
||||
|
||||
async syncCoValue(coValue: CoValueCore) {
|
||||
syncCoValue(coValue: CoValueCore) {
|
||||
this.requestedSyncs.delete(coValue.id);
|
||||
|
||||
for (const peer of this.peersInPriorityOrder()) {
|
||||
@@ -668,21 +714,21 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
async waitForSyncWithPeer(peerId: PeerID, id: RawCoID, timeout: number) {
|
||||
waitForSyncWithPeer(peerId: PeerID, id: RawCoID, timeout: number) {
|
||||
const { syncState } = this;
|
||||
const currentSyncState = syncState.getCurrentSyncState(peerId, id);
|
||||
|
||||
const isTheConditionAlreadyMet = currentSyncState.uploaded;
|
||||
|
||||
if (isTheConditionAlreadyMet) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
const peerState = this.peers[peerId];
|
||||
|
||||
// The peer has been closed, so it isn't possible to sync
|
||||
if (!peerState || peerState.closed) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
// The client isn't subscribed to the coValue, so we won't sync it
|
||||
@@ -690,7 +736,7 @@ export class SyncManager {
|
||||
peerState.role === "client" &&
|
||||
!peerState.optimisticKnownStates.has(id)
|
||||
) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -712,25 +758,25 @@ export class SyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
async waitForStorageSync(id: RawCoID, timeout = 30_000) {
|
||||
waitForStorageSync(id: RawCoID, timeout = 30_000) {
|
||||
const peers = this.getPeers();
|
||||
|
||||
await Promise.all(
|
||||
return Promise.all(
|
||||
peers
|
||||
.filter((peer) => peer.role === "storage")
|
||||
.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
|
||||
);
|
||||
}
|
||||
|
||||
async waitForSync(id: RawCoID, timeout = 30_000) {
|
||||
waitForSync(id: RawCoID, timeout = 30_000) {
|
||||
const peers = this.getPeers();
|
||||
|
||||
await Promise.all(
|
||||
return Promise.all(
|
||||
peers.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
|
||||
);
|
||||
}
|
||||
|
||||
async waitForAllCoValuesSync(timeout = 60_000) {
|
||||
waitForAllCoValuesSync(timeout = 60_000) {
|
||||
const coValues = this.local.allCoValues();
|
||||
const validCoValues = Array.from(coValues).filter(
|
||||
(coValue) =>
|
||||
|
||||
@@ -257,7 +257,7 @@ describe("SyncStateManager", () => {
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
|
||||
await expect(map.core.waitForSync()).resolves.toBeUndefined();
|
||||
await map.core.waitForSync();
|
||||
});
|
||||
|
||||
test("should skip client peers that are not subscribed to the coValue", async () => {
|
||||
|
||||
255
packages/cojson/src/tests/group.removeMember.test.ts
Normal file
255
packages/cojson/src/tests/group.removeMember.test.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { RawCoList } from "../coValues/coList.js";
|
||||
import { RawCoMap } from "../coValues/coMap.js";
|
||||
import { RawCoStream } from "../coValues/coStream.js";
|
||||
import { RawBinaryCoStream } from "../coValues/coStream.js";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
||||
import { RawAccountID } from "../exports.js";
|
||||
import {
|
||||
SyncMessagesLog,
|
||||
createTwoConnectedNodes,
|
||||
loadCoValueOrFail,
|
||||
setupTestAccount,
|
||||
setupTestNode,
|
||||
} from "./testUtils.js";
|
||||
|
||||
let jazzCloud = setupTestNode({ isSyncServer: true });
|
||||
|
||||
beforeEach(async () => {
|
||||
SyncMessagesLog.clear();
|
||||
jazzCloud = setupTestNode({ isSyncServer: true });
|
||||
});
|
||||
|
||||
describe("Group.removeMember", () => {
|
||||
test("a reader member should be able to revoke themselves", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const reader = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const readerOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
reader.accountID,
|
||||
);
|
||||
group.addMember(readerOnAdminNode, "reader");
|
||||
|
||||
const groupOnReaderNode = await loadCoValueOrFail(reader.node, group.id);
|
||||
expect(groupOnReaderNode.myRole()).toEqual("reader");
|
||||
|
||||
await groupOnReaderNode.removeMember(
|
||||
reader.node.expectCurrentAccount("reader"),
|
||||
);
|
||||
|
||||
expect(groupOnReaderNode.myRole()).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("a writer member should be able to revoke themselves", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const writer = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const writerOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
writer.accountID,
|
||||
);
|
||||
group.addMember(writerOnAdminNode, "writer");
|
||||
|
||||
const groupOnWriterNode = await loadCoValueOrFail(writer.node, group.id);
|
||||
expect(groupOnWriterNode.myRole()).toEqual("writer");
|
||||
|
||||
await groupOnWriterNode.removeMember(
|
||||
writer.node.expectCurrentAccount("writer"),
|
||||
);
|
||||
|
||||
expect(groupOnWriterNode.myRole()).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("a writeOnly member should be able to revoke themselves", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const writeOnly = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const writeOnlyOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
writeOnly.accountID,
|
||||
);
|
||||
group.addMember(writeOnlyOnAdminNode, "writeOnly");
|
||||
|
||||
const groupOnWriteOnlyNode = await loadCoValueOrFail(
|
||||
writeOnly.node,
|
||||
group.id,
|
||||
);
|
||||
expect(groupOnWriteOnlyNode.myRole()).toEqual("writeOnly");
|
||||
|
||||
await groupOnWriteOnlyNode.removeMember(
|
||||
writeOnly.node.expectCurrentAccount("writeOnly"),
|
||||
);
|
||||
|
||||
expect(groupOnWriteOnlyNode.myRole()).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("an admin member should be able to revoke themselves", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const otherAdmin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const otherAdminOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
otherAdmin.accountID,
|
||||
);
|
||||
group.addMember(otherAdminOnAdminNode, "admin");
|
||||
|
||||
const groupOnOtherAdminNode = await loadCoValueOrFail(
|
||||
otherAdmin.node,
|
||||
group.id,
|
||||
);
|
||||
expect(groupOnOtherAdminNode.myRole()).toEqual("admin");
|
||||
|
||||
await groupOnOtherAdminNode.removeMember(
|
||||
otherAdmin.node.expectCurrentAccount("admin"),
|
||||
);
|
||||
|
||||
expect(groupOnOtherAdminNode.myRole()).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("a writer member cannot remove other accounts", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const writer = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const otherMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const writerOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
writer.accountID,
|
||||
);
|
||||
const otherMemberOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
group.addMember(writerOnAdminNode, "writer");
|
||||
group.addMember(otherMemberOnAdminNode, "reader");
|
||||
|
||||
const groupOnWriterNode = await loadCoValueOrFail(writer.node, group.id);
|
||||
expect(groupOnWriterNode.myRole()).toEqual("writer");
|
||||
|
||||
const otherMemberOnWriterNode = await loadCoValueOrFail(
|
||||
writer.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
await groupOnWriterNode.removeMember(otherMemberOnWriterNode);
|
||||
|
||||
expect(groupOnWriterNode.roleOf(otherMember.accountID)).toEqual("reader");
|
||||
});
|
||||
|
||||
test("a writeOnly member cannot remove other accounts", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const writeOnly = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const otherMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const writeOnlyOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
writeOnly.accountID,
|
||||
);
|
||||
const otherMemberOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
group.addMember(writeOnlyOnAdminNode, "writeOnly");
|
||||
group.addMember(otherMemberOnAdminNode, "reader");
|
||||
|
||||
const groupOnWriteOnlyNode = await loadCoValueOrFail(
|
||||
writeOnly.node,
|
||||
group.id,
|
||||
);
|
||||
expect(groupOnWriteOnlyNode.myRole()).toEqual("writeOnly");
|
||||
|
||||
const otherMemberOnWriteOnlyNode = await loadCoValueOrFail(
|
||||
writeOnly.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
await groupOnWriteOnlyNode.removeMember(otherMemberOnWriteOnlyNode);
|
||||
|
||||
expect(groupOnWriteOnlyNode.roleOf(otherMember.accountID)).toEqual(
|
||||
"reader",
|
||||
);
|
||||
});
|
||||
|
||||
test("a reader member cannot remove other accounts", async () => {
|
||||
const admin = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const reader = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const otherMember = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = admin.node.createGroup();
|
||||
const readerOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
reader.accountID,
|
||||
);
|
||||
const otherMemberOnAdminNode = await loadCoValueOrFail(
|
||||
admin.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
group.addMember(readerOnAdminNode, "reader");
|
||||
group.addMember(otherMemberOnAdminNode, "writer");
|
||||
|
||||
const groupOnReaderNode = await loadCoValueOrFail(reader.node, group.id);
|
||||
expect(groupOnReaderNode.myRole()).toEqual("reader");
|
||||
|
||||
const otherMemberOnReaderNode = await loadCoValueOrFail(
|
||||
reader.node,
|
||||
otherMember.accountID,
|
||||
);
|
||||
|
||||
await groupOnReaderNode.removeMember(otherMemberOnReaderNode);
|
||||
|
||||
expect(groupOnReaderNode.roleOf(otherMember.accountID)).toEqual("writer");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { expectMap } from "../coValue";
|
||||
import {
|
||||
SyncMessagesLog,
|
||||
blockMessageTypeOnOutgoingPeer,
|
||||
connectedPeersWithMessagesTracking,
|
||||
loadCoValueOrFail,
|
||||
setupTestNode,
|
||||
waitFor,
|
||||
@@ -76,8 +77,8 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
|
||||
"core -> storage | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> core | KNOWN Group sessions: header/3",
|
||||
"core -> storage | CONTENT Map header: true new: After: 0 New: 1",
|
||||
"storage -> core | KNOWN Map sessions: header/1",
|
||||
"client -> edge-italy | LOAD Map sessions: empty",
|
||||
"storage -> core | KNOWN Map sessions: header/1",
|
||||
"edge-italy -> core | LOAD Map sessions: empty",
|
||||
"core -> edge-italy | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"edge-italy -> core | KNOWN Group sessions: header/3",
|
||||
@@ -138,8 +139,8 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
|
||||
"core -> storage | CONTENT Group header: true new: After: 0 New: 5",
|
||||
"storage -> core | KNOWN Group sessions: header/5",
|
||||
"core -> storage | CONTENT Map header: true new: After: 0 New: 1",
|
||||
"storage -> core | KNOWN Map sessions: header/1",
|
||||
"client -> edge-italy | LOAD Map sessions: empty",
|
||||
"storage -> core | KNOWN Map sessions: header/1",
|
||||
"edge-italy -> core | LOAD Map sessions: empty",
|
||||
"core -> edge-italy | CONTENT ParentGroup header: true new: After: 0 New: 6",
|
||||
"edge-italy -> core | KNOWN ParentGroup sessions: header/6",
|
||||
@@ -355,6 +356,7 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
|
||||
syncServer: storage.node,
|
||||
});
|
||||
|
||||
storagePeer.role = "storage";
|
||||
storagePeer.priority = 100;
|
||||
|
||||
const group = coreServer.node.createGroup();
|
||||
@@ -402,4 +404,60 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
|
||||
|
||||
expect(mapOnClient.get("hello")).toEqual("world");
|
||||
});
|
||||
|
||||
test("a stuck server peer should not block the load from other server peers", async () => {
|
||||
const client = setupTestNode();
|
||||
const coreServer = setupTestNode({
|
||||
isSyncServer: true,
|
||||
});
|
||||
|
||||
const anotherServer = setupTestNode({});
|
||||
|
||||
const { peer: peerToCoreServer } = client.connectToSyncServer({
|
||||
syncServerName: "core",
|
||||
syncServer: coreServer.node,
|
||||
});
|
||||
|
||||
const { peer1, peer2 } = connectedPeersWithMessagesTracking({
|
||||
peer1: {
|
||||
id: anotherServer.node.getCurrentAgent().id,
|
||||
role: "server",
|
||||
name: "another-server",
|
||||
},
|
||||
peer2: {
|
||||
id: client.node.getCurrentAgent().id,
|
||||
role: "client",
|
||||
name: "client",
|
||||
},
|
||||
});
|
||||
|
||||
blockMessageTypeOnOutgoingPeer(peerToCoreServer, "load");
|
||||
|
||||
client.node.syncManager.addPeer(peer1);
|
||||
anotherServer.node.syncManager.addPeer(peer2);
|
||||
|
||||
const group = anotherServer.node.createGroup();
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("hello", "world", "trusting");
|
||||
|
||||
const mapOnClient = await loadCoValueOrFail(client.node, map.id);
|
||||
|
||||
expect(
|
||||
SyncMessagesLog.getMessages({
|
||||
Group: group.core,
|
||||
Map: map.core,
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> another-server | LOAD Map sessions: empty",
|
||||
"another-server -> client | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> another-server | KNOWN Group sessions: header/3",
|
||||
"another-server -> client | CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> another-server | KNOWN Map sessions: header/1",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(mapOnClient.get("hello")).toEqual("world");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { assert, beforeEach, describe, expect, test } from "vitest";
|
||||
import { expectMap } from "../coValue";
|
||||
import { WasmCrypto } from "../crypto/WasmCrypto";
|
||||
import { SyncMessagesLog, setupTestNode, waitFor } from "./testUtils";
|
||||
import {
|
||||
SyncMessagesLog,
|
||||
loadCoValueOrFail,
|
||||
setupTestAccount,
|
||||
setupTestNode,
|
||||
waitFor,
|
||||
} from "./testUtils";
|
||||
|
||||
let jazzCloud = setupTestNode({ isSyncServer: true });
|
||||
|
||||
@@ -146,7 +152,9 @@ describe("peer reconciliation", () => {
|
||||
});
|
||||
|
||||
test("correctly handle server restarts in the middle of a sync", async () => {
|
||||
const client = setupTestNode();
|
||||
const client = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
@@ -182,9 +190,14 @@ describe("peer reconciliation", () => {
|
||||
"server -> client | KNOWN Group sessions: empty",
|
||||
"client -> server | LOAD Map sessions: header/2",
|
||||
"server -> client | KNOWN Map sessions: empty",
|
||||
"client -> server | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"server -> client | KNOWN Group sessions: header/3",
|
||||
"client -> server | CONTENT Map header: false new: After: 1 New: 1",
|
||||
"server -> client | KNOWN CORRECTION Map sessions: empty",
|
||||
"client -> server | CONTENT Map header: true new: After: 0 New: 2",
|
||||
"server -> client | LOAD Group sessions: empty",
|
||||
"client -> server | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"server -> client | KNOWN CORRECTION Map sessions: empty",
|
||||
"client -> server | CONTENT Map header: true new: After: 0 New: 2",
|
||||
"server -> client | KNOWN Group sessions: header/3",
|
||||
"server -> client | KNOWN Map sessions: header/2",
|
||||
"client -> server | LOAD Group sessions: header/3",
|
||||
"server -> client | KNOWN Group sessions: header/3",
|
||||
@@ -194,6 +207,81 @@ describe("peer reconciliation", () => {
|
||||
`);
|
||||
});
|
||||
|
||||
test("correctly handle server restarts in the middle of a sync (2 - account)", async () => {
|
||||
const client = await setupTestAccount({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
const group = client.node.createGroup();
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("hello", "world", "trusting");
|
||||
|
||||
await map.core.waitForSync();
|
||||
|
||||
jazzCloud.restart();
|
||||
SyncMessagesLog.clear();
|
||||
client.connectToSyncServer();
|
||||
|
||||
map.set("hello", "updated", "trusting");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
client.connectToSyncServer();
|
||||
|
||||
await waitFor(() => {
|
||||
const mapOnSyncServer = jazzCloud.node.getCoValue(map.id);
|
||||
|
||||
expect(mapOnSyncServer.loadingState).toBe("available");
|
||||
});
|
||||
|
||||
expect(
|
||||
SyncMessagesLog.getMessages({
|
||||
Account: client.node.expectCurrentAccount("client account").core,
|
||||
Profile: client.node.expectProfileLoaded(client.accountID).core,
|
||||
ProfileGroup: client.node.expectProfileLoaded(client.accountID).group
|
||||
.core,
|
||||
Group: group.core,
|
||||
Map: map.core,
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> server | LOAD Account sessions: header/4",
|
||||
"server -> client | KNOWN Account sessions: empty",
|
||||
"client -> server | LOAD ProfileGroup sessions: header/5",
|
||||
"server -> client | KNOWN ProfileGroup sessions: empty",
|
||||
"client -> server | LOAD Profile sessions: header/1",
|
||||
"server -> client | KNOWN Profile sessions: empty",
|
||||
"client -> server | LOAD Group sessions: header/3",
|
||||
"server -> client | KNOWN Group sessions: empty",
|
||||
"client -> server | LOAD Map sessions: header/2",
|
||||
"server -> client | KNOWN Map sessions: empty",
|
||||
"client -> server | CONTENT Map header: false new: After: 1 New: 1",
|
||||
"server -> client | KNOWN CORRECTION Map sessions: empty",
|
||||
"client -> server | CONTENT Map header: true new: After: 0 New: 2",
|
||||
"server -> client | LOAD Account sessions: empty",
|
||||
"client -> server | CONTENT Account header: true new: After: 0 New: 4",
|
||||
"server -> client | LOAD Group sessions: empty",
|
||||
"client -> server | CONTENT Group header: true new: After: 0 New: 3",
|
||||
"server -> client | KNOWN CORRECTION Map sessions: empty",
|
||||
"client -> server | CONTENT Map header: true new: After: 0 New: 2",
|
||||
"server -> client | KNOWN Account sessions: header/4",
|
||||
"server -> client | KNOWN Group sessions: header/3",
|
||||
"server -> client | KNOWN Map sessions: header/2",
|
||||
"client -> server | LOAD Account sessions: header/4",
|
||||
"server -> client | KNOWN Account sessions: header/4",
|
||||
"client -> server | LOAD ProfileGroup sessions: header/5",
|
||||
"server -> client | KNOWN ProfileGroup sessions: empty",
|
||||
"client -> server | LOAD Profile sessions: header/1",
|
||||
"server -> client | KNOWN Profile sessions: empty",
|
||||
"client -> server | LOAD Group sessions: header/3",
|
||||
"server -> client | KNOWN Group sessions: header/3",
|
||||
"client -> server | LOAD Map sessions: header/2",
|
||||
"server -> client | KNOWN Map sessions: header/2",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test.skip("handle peer reconnections with data loss", async () => {
|
||||
const client = setupTestNode();
|
||||
|
||||
|
||||
@@ -463,6 +463,7 @@ export function createMockStoragePeer(opts: {
|
||||
},
|
||||
});
|
||||
|
||||
peer1.role = "storage";
|
||||
peer1.priority = 100;
|
||||
|
||||
storage.syncManager.addPeer(peer2);
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-auth-clerk
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- jazz-browser@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- jazz-browser@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "jazz-auth-clerk",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.13.25",
|
||||
"jazz-browser": "workspace:0.13.26",
|
||||
"jazz-tools": "workspace:0.13.26"
|
||||
"cojson": "workspace:0.13.28",
|
||||
"jazz-browser": "workspace:0.13.28",
|
||||
"jazz-tools": "workspace:0.13.28"
|
||||
},
|
||||
"scripts": {
|
||||
"format-and-lint": "biome check .",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -8,8 +8,8 @@
|
||||
"dependencies": {
|
||||
"@types/image-blob-reduce": "^4.1.1",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "workspace:0.13.26",
|
||||
"jazz-tools": "workspace:0.13.26",
|
||||
"jazz-browser": "workspace:0.13.28",
|
||||
"jazz-tools": "workspace:0.13.28",
|
||||
"pica": "^9.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- Updated dependencies [422dbc4]
|
||||
- cojson@0.13.28
|
||||
- cojson-transport-ws@0.13.28
|
||||
- cojson-storage-indexeddb@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-storage-indexeddb@0.13.27
|
||||
- cojson-transport-ws@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- Updated dependencies [422dbc4]
|
||||
- cojson@0.13.28
|
||||
- cojson-transport-ws@0.13.28
|
||||
- jazz-auth-clerk@0.13.28
|
||||
- jazz-react-core@0.13.28
|
||||
- jazz-react-native-core@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-transport-ws@0.13.27
|
||||
- jazz-auth-clerk@0.13.27
|
||||
- jazz-react-core@0.13.27
|
||||
- jazz-react-native-core@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-expo",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-inspector-element
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-inspector@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-inspector-element",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "./dist/main.js",
|
||||
"types": "./dist/main.d.ts",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-inspector
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- jazz-react-core@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- jazz-react-core@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "./dist/app.js",
|
||||
"types": "./dist/app.d.ts",
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# jazz-autosub
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 422dbc4: Add waitForConnection and subscribeToConnectionChange APIs to handle connection drops
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- Updated dependencies [422dbc4]
|
||||
- cojson@0.13.28
|
||||
- cojson-transport-ws@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- cojson-transport-ws@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"cojson-transport-ws": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { AgentSecret, CryptoProvider, LocalNode } from "cojson";
|
||||
import { type AnyWebSocketConstructor } from "cojson-transport-ws";
|
||||
import { AgentSecret, CryptoProvider, LocalNode, Peer } from "cojson";
|
||||
import {
|
||||
type AnyWebSocketConstructor,
|
||||
WebSocketPeerWithReconnection,
|
||||
} from "cojson-transport-ws";
|
||||
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import {
|
||||
Account,
|
||||
@@ -9,7 +12,6 @@ import {
|
||||
createJazzContextFromExistingCredentials,
|
||||
randomSessionProvider,
|
||||
} from "jazz-tools";
|
||||
import { webSocketWithReconnection } from "./webSocketWithReconnection.js";
|
||||
|
||||
type WorkerOptions<Acc extends Account> = {
|
||||
accountID?: string;
|
||||
@@ -32,13 +34,24 @@ export async function startWorker<Acc extends Account>(
|
||||
} = options;
|
||||
|
||||
let node: LocalNode | undefined = undefined;
|
||||
const wsPeer = webSocketWithReconnection(
|
||||
syncServer,
|
||||
(peer) => {
|
||||
node?.syncManager.addPeer(peer);
|
||||
|
||||
const peersToLoadFrom: Peer[] = [];
|
||||
|
||||
const wsPeer = new WebSocketPeerWithReconnection({
|
||||
peer: syncServer,
|
||||
reconnectionTimeout: 100,
|
||||
addPeer: (peer) => {
|
||||
if (node) {
|
||||
node.syncManager.addPeer(peer);
|
||||
} else {
|
||||
peersToLoadFrom.push(peer);
|
||||
}
|
||||
},
|
||||
options.WebSocket,
|
||||
);
|
||||
removePeer: () => {},
|
||||
WebSocketConstructor: options.WebSocket,
|
||||
});
|
||||
|
||||
wsPeer.enable();
|
||||
|
||||
if (!accountID) {
|
||||
throw new Error("No accountID provided");
|
||||
@@ -61,7 +74,7 @@ export async function startWorker<Acc extends Account>(
|
||||
AccountSchema,
|
||||
// TODO: locked sessions similar to browser
|
||||
sessionProvider: randomSessionProvider,
|
||||
peersToLoadFrom: [wsPeer.peer],
|
||||
peersToLoadFrom,
|
||||
crypto: options.crypto ?? (await WasmCrypto.create()),
|
||||
});
|
||||
|
||||
@@ -77,7 +90,7 @@ export async function startWorker<Acc extends Account>(
|
||||
async function done() {
|
||||
await context.account.waitForAllCoValuesSync();
|
||||
|
||||
wsPeer.done();
|
||||
wsPeer.disable();
|
||||
context.done();
|
||||
}
|
||||
|
||||
@@ -90,6 +103,16 @@ export async function startWorker<Acc extends Account>(
|
||||
experimental: {
|
||||
inbox: inboxPublicApi,
|
||||
},
|
||||
waitForConnection() {
|
||||
return wsPeer.waitUntilConnected();
|
||||
},
|
||||
subscribeToConnectionChange(listener: (connected: boolean) => void) {
|
||||
wsPeer.subscribe(listener);
|
||||
|
||||
return () => {
|
||||
wsPeer.unsubscribe(listener);
|
||||
};
|
||||
},
|
||||
done,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createWorkerAccount } from "jazz-run/createWorkerAccount";
|
||||
import { startSyncServer } from "jazz-run/startSyncServer";
|
||||
import {
|
||||
@@ -8,24 +12,40 @@ import {
|
||||
InboxSender,
|
||||
co,
|
||||
} from "jazz-tools";
|
||||
import { describe, expect, onTestFinished, test } from "vitest";
|
||||
import { afterAll, describe, expect, onTestFinished, test } from "vitest";
|
||||
import { startWorker } from "../index.js";
|
||||
import { waitFor } from "./utils.js";
|
||||
|
||||
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
|
||||
|
||||
afterAll(() => {
|
||||
unlinkSync(dbPath);
|
||||
});
|
||||
|
||||
async function setup<Acc extends Account>(AccountSchema?: AccountClass<Acc>) {
|
||||
const { server, port } = await setupSyncServer();
|
||||
|
||||
const syncServer = `ws://localhost:${port}`;
|
||||
|
||||
const { worker, done } = await setupWorker(syncServer, AccountSchema);
|
||||
const { worker, done, waitForConnection, subscribeToConnectionChange } =
|
||||
await setupWorker(syncServer, AccountSchema);
|
||||
|
||||
return { worker, done, syncServer, server, port };
|
||||
return {
|
||||
worker,
|
||||
done,
|
||||
syncServer,
|
||||
server,
|
||||
port,
|
||||
waitForConnection,
|
||||
subscribeToConnectionChange,
|
||||
};
|
||||
}
|
||||
|
||||
async function setupSyncServer(defaultPort = "0") {
|
||||
const server = await startSyncServer({
|
||||
port: defaultPort,
|
||||
inMemory: true,
|
||||
db: "",
|
||||
inMemory: false,
|
||||
db: dbPath,
|
||||
});
|
||||
|
||||
const port = (server.address() as { port: number }).port.toString();
|
||||
@@ -225,6 +245,8 @@ describe("startWorker integration", () => {
|
||||
{ owner: group },
|
||||
);
|
||||
|
||||
map.value = "updated while offline";
|
||||
|
||||
// Start a new sync server on the same port
|
||||
const newServer = await startSyncServer({
|
||||
port: worker1.port,
|
||||
@@ -232,8 +254,11 @@ describe("startWorker integration", () => {
|
||||
db: "",
|
||||
});
|
||||
|
||||
// Wait for reconnection and sync
|
||||
await map2.waitForSync();
|
||||
// Wait for reconnection
|
||||
await worker1.waitForConnection();
|
||||
await worker2.waitForConnection();
|
||||
|
||||
await worker1.worker.waitForAllCoValuesSync();
|
||||
|
||||
// Verify both old and new values are synced
|
||||
const mapOnWorker2 = await TestMap.load(map.id, { loadAs: worker2.worker });
|
||||
@@ -241,11 +266,74 @@ describe("startWorker integration", () => {
|
||||
loadAs: worker2.worker,
|
||||
});
|
||||
|
||||
expect(mapOnWorker2?.value).toBe("initial value");
|
||||
expect(mapOnWorker2?.value).toBe("updated while offline");
|
||||
expect(map2OnWorker2?.value).toBe("created while offline");
|
||||
|
||||
// Cleanup
|
||||
await worker2.done();
|
||||
newServer.close();
|
||||
});
|
||||
|
||||
test("waitForConnection resolves when connection is established", async () => {
|
||||
const worker1 = await setup();
|
||||
|
||||
// Initially should be connected
|
||||
await worker1.waitForConnection();
|
||||
|
||||
// Close the sync server
|
||||
worker1.server.close();
|
||||
|
||||
// Start a new sync server on the same port
|
||||
const newServer = await startSyncServer({
|
||||
port: worker1.port,
|
||||
inMemory: true,
|
||||
db: "",
|
||||
});
|
||||
|
||||
// Should reconnect and resolve
|
||||
await worker1.waitForConnection();
|
||||
|
||||
// Cleanup
|
||||
await worker1.done();
|
||||
newServer.close();
|
||||
});
|
||||
|
||||
test("subscribeToConnectionChange notifies on connection state changes", async () => {
|
||||
const worker1 = await setup();
|
||||
|
||||
const connectionStates: boolean[] = [];
|
||||
|
||||
// Subscribe to connection changes
|
||||
const unsubscribe = worker1.subscribeToConnectionChange((isConnected) => {
|
||||
connectionStates.push(isConnected);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(connectionStates).toEqual([true]);
|
||||
});
|
||||
|
||||
// Close the sync server
|
||||
worker1.server.close();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(connectionStates).toEqual([true, false]);
|
||||
});
|
||||
|
||||
// Start a new sync server on the same port
|
||||
const newServer = await startSyncServer({
|
||||
port: worker1.port,
|
||||
inMemory: true,
|
||||
db: "",
|
||||
});
|
||||
|
||||
// Wait a bit for the reconnection to be detected
|
||||
await waitFor(() => {
|
||||
expect(connectionStates).toEqual([true, false, true]);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
unsubscribe();
|
||||
await worker1.done();
|
||||
newServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { createWebSocketPeer } from "cojson-transport-ws";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { webSocketWithReconnection } from "../webSocketWithReconnection.js";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("cojson-transport-ws", () => ({
|
||||
createWebSocketPeer: vi.fn().mockImplementation(({ onClose }) => ({
|
||||
id: "upstream",
|
||||
incoming: { push: vi.fn() },
|
||||
outgoing: { push: vi.fn(), close: vi.fn() },
|
||||
onClose,
|
||||
})),
|
||||
}));
|
||||
|
||||
const WebSocketMock = vi.fn().mockImplementation(() => ({
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
close: vi.fn(),
|
||||
readyState: 1,
|
||||
})) as unknown as typeof WebSocket;
|
||||
|
||||
describe("webSocketWithReconnection", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("should create initial websocket connection", () => {
|
||||
const addPeerMock = vi.fn();
|
||||
const { peer } = webSocketWithReconnection(
|
||||
"ws://localhost:8080",
|
||||
addPeerMock,
|
||||
WebSocketMock,
|
||||
);
|
||||
|
||||
expect(WebSocketMock).toHaveBeenCalledWith("ws://localhost:8080");
|
||||
expect(createWebSocketPeer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "upstream",
|
||||
role: "server",
|
||||
}),
|
||||
);
|
||||
expect(peer).toBeDefined();
|
||||
});
|
||||
|
||||
test("should attempt reconnection when websocket closes", async () => {
|
||||
const addPeerMock = vi.fn();
|
||||
webSocketWithReconnection(
|
||||
"ws://localhost:8080",
|
||||
addPeerMock,
|
||||
WebSocketMock,
|
||||
);
|
||||
|
||||
// Get the onClose handler from the first createWebSocketPeer call
|
||||
const initialPeer = vi.mocked(createWebSocketPeer).mock.results[0]!.value;
|
||||
|
||||
// Simulate websocket close
|
||||
initialPeer.onClose();
|
||||
|
||||
// Fast-forward timer to trigger reconnection
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(WebSocketMock).toHaveBeenCalledTimes(2);
|
||||
expect(createWebSocketPeer).toHaveBeenCalledTimes(2);
|
||||
expect(addPeerMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "upstream",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should clean up when done is called", () => {
|
||||
const addPeerMock = vi.fn();
|
||||
const { done } = webSocketWithReconnection(
|
||||
"ws://localhost:8080",
|
||||
addPeerMock,
|
||||
WebSocketMock,
|
||||
);
|
||||
|
||||
// Get the onClose handler
|
||||
const initialPeer = vi.mocked(createWebSocketPeer).mock.results[0]!.value;
|
||||
|
||||
done();
|
||||
|
||||
// Simulate websocket close
|
||||
initialPeer.onClose();
|
||||
|
||||
// Fast-forward timer
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
// Should not attempt reconnection
|
||||
expect(WebSocketMock).toHaveBeenCalledTimes(1);
|
||||
expect(createWebSocketPeer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should not attempt reconnection after done is called", async () => {
|
||||
const addPeerMock = vi.fn();
|
||||
const { done } = webSocketWithReconnection(
|
||||
"ws://localhost:8080",
|
||||
addPeerMock,
|
||||
WebSocketMock,
|
||||
);
|
||||
|
||||
// Get the onClose handler
|
||||
const initialPeer = vi.mocked(createWebSocketPeer).mock.results[0]!.value;
|
||||
|
||||
// Simulate first close and reconnection
|
||||
initialPeer.onClose();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(WebSocketMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Call done
|
||||
done();
|
||||
|
||||
// Simulate another close
|
||||
vi.mocked(createWebSocketPeer).mock.results[1]!.value.onClose();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
// Should not create another connection
|
||||
expect(WebSocketMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Peer } from "cojson";
|
||||
import {
|
||||
AnyWebSocketConstructor,
|
||||
createWebSocketPeer,
|
||||
} from "cojson-transport-ws";
|
||||
|
||||
export function webSocketWithReconnection(
|
||||
peer: string,
|
||||
addPeer: (peer: Peer) => void,
|
||||
ws?: AnyWebSocketConstructor,
|
||||
) {
|
||||
let done = false;
|
||||
|
||||
const WebSocketConstructor = ws ?? WebSocket;
|
||||
|
||||
const wsPeer = createWebSocketPeer({
|
||||
websocket: new WebSocketConstructor(peer),
|
||||
id: "upstream",
|
||||
role: "server",
|
||||
onClose: handleClose,
|
||||
});
|
||||
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
function handleClose() {
|
||||
if (done) return;
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const wsPeer: Peer = createWebSocketPeer({
|
||||
id: "upstream",
|
||||
websocket: new WebSocketConstructor(peer),
|
||||
role: "server",
|
||||
onClose: handleClose,
|
||||
});
|
||||
|
||||
addPeer(wsPeer);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
peer: wsPeer,
|
||||
done: () => {
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- jazz-auth-clerk@0.13.28
|
||||
- jazz-browser@0.13.28
|
||||
- jazz-react@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- jazz-auth-clerk@0.13.27
|
||||
- jazz-browser@0.13.27
|
||||
- jazz-react@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-auth-clerk",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# jazz-react-core
|
||||
|
||||
## 0.13.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e7ccb2c]
|
||||
- cojson@0.13.28
|
||||
- jazz-tools@0.13.28
|
||||
|
||||
## 0.13.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6357052]
|
||||
- cojson@0.13.27
|
||||
- jazz-tools@0.13.27
|
||||
|
||||
## 0.13.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-core",
|
||||
"version": "0.13.26",
|
||||
"version": "0.13.28",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user