Compare commits
51 Commits
cojson-tra
...
jazz-nodej
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb158d5fa | ||
|
|
8b87117e0f | ||
|
|
e5000c2b6b | ||
|
|
0347e52118 | ||
|
|
bb9ba33e73 | ||
|
|
92c63d94b9 | ||
|
|
75339c0939 | ||
|
|
22102deabc | ||
|
|
760a1c10c5 | ||
|
|
fac73f0e44 | ||
|
|
80b04e78c9 | ||
|
|
b23c0d75fe | ||
|
|
d4319d8a0e | ||
|
|
52cd4a9a0f | ||
|
|
ac241981f6 | ||
|
|
b3504dce9c | ||
|
|
f859d9f1c5 | ||
|
|
d433cf473f | ||
|
|
c6cd6cba20 | ||
|
|
9c7333d4f0 | ||
|
|
970f870245 | ||
|
|
a2dbaf86d2 | ||
|
|
b6162f0fc4 | ||
|
|
91cfa6aa8f | ||
|
|
4fd6bbd41d | ||
|
|
0ea3ef31ac | ||
|
|
d02613e2ec | ||
|
|
8ff036e5ff | ||
|
|
d0733e2a3b | ||
|
|
ad4f1b74f6 | ||
|
|
04d97e4a2d | ||
|
|
3f9e749122 | ||
|
|
aa1dd3e10a | ||
|
|
043e2acae4 | ||
|
|
1b7ef1c2c0 | ||
|
|
996092c26f | ||
|
|
3c794bba0a | ||
|
|
2d3d53d144 | ||
|
|
69d05c8c15 | ||
|
|
d11aeee083 | ||
|
|
4d5848161d | ||
|
|
8ee456d4e4 | ||
|
|
7b2c2e6084 | ||
|
|
133f75d34e | ||
|
|
68c9114896 | ||
|
|
57ff9e2d1f | ||
|
|
1cac820ec6 | ||
|
|
eb4646beca | ||
|
|
5447d6f10b | ||
|
|
dbb040eb07 | ||
|
|
9960320645 |
39
.changeset/pre.json
Normal file
39
.changeset/pre.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "group-inheritance",
|
||||
"initialVersions": {
|
||||
"@jazz-e2e/binarycostream": "0.0.96",
|
||||
"@jazz-e2e/covalues": "0.0.95",
|
||||
"jazz-example-book-shelf": "0.1.11",
|
||||
"jazz-example-chat": "0.0.97",
|
||||
"jazz-example-chat-clerk": "0.0.95",
|
||||
"chat-rn": "1.0.13",
|
||||
"chat-rn-clerk": "1.0.11",
|
||||
"chat-vue": "0.0.3",
|
||||
"jazz-inspector": "0.0.71",
|
||||
"jazz-example-music-player": "0.0.17",
|
||||
"jazz-password-manager": "0.0.16",
|
||||
"jazz-example-pets": "0.0.114",
|
||||
"richtext": "0.0.0",
|
||||
"jazz-example-todo": "0.0.113",
|
||||
"cojson": "0.8.18",
|
||||
"cojson-storage-indexeddb": "0.8.18",
|
||||
"cojson-storage-sqlite": "0.8.18",
|
||||
"cojson-transport-ws": "0.8.18",
|
||||
"hash-slash": "0.2.1",
|
||||
"jazz-browser": "0.8.18",
|
||||
"jazz-browser-auth-clerk": "0.8.18",
|
||||
"jazz-browser-media-images": "0.8.18",
|
||||
"jazz-nodejs": "0.8.18",
|
||||
"jazz-react": "0.8.18",
|
||||
"jazz-react-auth-clerk": "0.8.18",
|
||||
"jazz-react-native": "0.8.18",
|
||||
"jazz-react-native-media-images": "0.8.14",
|
||||
"jazz-run": "0.8.18",
|
||||
"jazz-tools": "0.8.18",
|
||||
"jazz-vue": "0.8.8"
|
||||
},
|
||||
"changesets": [
|
||||
"real-steaks-drum"
|
||||
]
|
||||
}
|
||||
8
.changeset/real-steaks-drum.md
Normal file
8
.changeset/real-steaks-drum.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"cojson-storage-indexeddb": patch
|
||||
"cojson-storage-sqlite": patch
|
||||
"jazz-tools": patch
|
||||
"cojson": patch
|
||||
---
|
||||
|
||||
Implement Group Inheritance
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": []
|
||||
"ignore": ["jazz-tools.json"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# @jazz-e2e/binarycostream
|
||||
|
||||
## 0.0.97-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@jazz-e2e/binarycostream",
|
||||
"private": true,
|
||||
"version": "0.0.94",
|
||||
"version": "0.0.97-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,11 +13,11 @@
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# @jazz-e2e/covalues
|
||||
|
||||
## 0.0.96-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@jazz-e2e/covalues",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.96-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-book-shelf
|
||||
|
||||
## 0.1.12-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-browser-media-images@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
- jazz-browser-media-images@0.8.18
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
- jazz-browser-media-images@0.8.17
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-example-book-shelf",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.12-group-inheritance.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,9 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-browser-media-images": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.96-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
- jazz-react-auth-clerk@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-react@0.8.18
|
||||
- jazz-react-auth-clerk@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-react@0.8.17
|
||||
- jazz-react-auth-clerk@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat-clerk",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.96-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -17,11 +17,11 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-react-auth-clerk": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.12-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-react-auth-clerk@0.8.19-group-inheritance.0
|
||||
- jazz-react-native@0.8.19-group-inheritance.0
|
||||
- jazz-react-native-media-images@0.8.15-group-inheritance.0
|
||||
|
||||
## 1.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-auth-clerk@0.8.18
|
||||
- jazz-react-native@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
- jazz-react-native-media-images@0.8.14
|
||||
|
||||
## 1.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-auth-clerk@0.8.17
|
||||
- jazz-react-native@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
- jazz-react-native-media-images@0.8.13
|
||||
|
||||
## 1.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.12-group-inheritance.0",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.14-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-react-native@0.8.19-group-inheritance.0
|
||||
|
||||
## 1.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 1.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 1.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.14-group-inheritance.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.4-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-browser@0.8.19-group-inheritance.0
|
||||
- jazz-vue@0.8.9-group-inheritance.0
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
- jazz-vue@0.8.8
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
- jazz-vue@0.8.7
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4-group-inheritance.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.98-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.95",
|
||||
"version": "0.0.98-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -18,10 +18,10 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.72-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- cojson-transport-ws@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- cojson-transport-ws@0.8.18
|
||||
|
||||
## 0.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- Updated dependencies [b6162f0]
|
||||
- cojson@0.8.17
|
||||
- cojson-transport-ws@0.8.17
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector",
|
||||
"private": true,
|
||||
"version": "0.0.69",
|
||||
"version": "0.0.72-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson-transport-ws": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
|
||||
"hash-slash": "workspace:0.2.1",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.18-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.18-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -18,8 +18,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.17-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.14",
|
||||
"version": "0.0.17-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.115-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-browser-media-images@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
- jazz-browser-media-images@0.8.18
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
- jazz-browser-media-images@0.8.17
|
||||
|
||||
## 0.0.112
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.112",
|
||||
"version": "0.0.115-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,9 +19,9 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-browser-media-images": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-browser-media-images": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.8.16",
|
||||
"jazz-run": "workspace:0.8.19-group-inheritance.0",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.114-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.0.112
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.0.111
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.111",
|
||||
"version": "0.0.114-group-inheritance.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b87117: Implement Group Inheritance
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -692,9 +692,19 @@ function getDependedOnCoValues(
|
||||
"key" in change &&
|
||||
change.key,
|
||||
)
|
||||
.filter(
|
||||
(key): key is CojsonInternalTypes.RawCoID =>
|
||||
typeof key === "string" && key.startsWith("co_"),
|
||||
.flatMap((key) =>
|
||||
typeof key === "string"
|
||||
? key.startsWith("co_")
|
||||
? [key as CojsonInternalTypes.RawCoID]
|
||||
: key.startsWith("parent_co_")
|
||||
? [
|
||||
key.replace(
|
||||
"parent_",
|
||||
"",
|
||||
) as CojsonInternalTypes.RawCoID,
|
||||
]
|
||||
: []
|
||||
: [],
|
||||
);
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b87117: Implement Group Inheritance
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.5.2",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -392,9 +392,19 @@ export class SQLiteStorage {
|
||||
"key" in change &&
|
||||
change.key,
|
||||
)
|
||||
.filter(
|
||||
(key): key is CojsonInternalTypes.RawCoID =>
|
||||
typeof key === "string" && key.startsWith("co_"),
|
||||
.flatMap((key) =>
|
||||
typeof key === "string"
|
||||
? key.startsWith("co_")
|
||||
? [key as CojsonInternalTypes.RawCoID]
|
||||
: key.startsWith("parent_co_")
|
||||
? [
|
||||
key.replace(
|
||||
"parent_",
|
||||
"",
|
||||
) as CojsonInternalTypes.RawCoID,
|
||||
]
|
||||
: []
|
||||
: [],
|
||||
);
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b6162f0: Less noisy logs if clients send empty WebSocket messages
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -147,6 +147,11 @@ export function createWebSocketPeer({
|
||||
);
|
||||
|
||||
function handleIncomingMsg(event: { data: unknown }) {
|
||||
if (event.data === "") {
|
||||
console.log("client", id, "sent empty message");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = deserializeMessages(event.data);
|
||||
|
||||
if (!result.ok) {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# cojson
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b87117: Implement Group Inheritance
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d4319d8: Immediately ack new content before syncing it upstream
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d433cf4: Add a new API to wait for a CoValue to be uploaded in a peer
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.3",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -66,6 +66,12 @@ export class PeerKnownStates {
|
||||
return this.coValues.has(id);
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new PeerKnownStates();
|
||||
clone.coValues = new Map(this.coValues);
|
||||
return clone;
|
||||
}
|
||||
|
||||
dispatch(action: PeerKnownStateActions) {
|
||||
switch (action.type) {
|
||||
case "UPDATE_HEADER":
|
||||
|
||||
@@ -8,9 +8,29 @@ import { CO_VALUE_PRIORITY } from "./priority.js";
|
||||
import { Peer, SyncMessage } from "./sync.js";
|
||||
|
||||
export class PeerState {
|
||||
constructor(private peer: Peer) {}
|
||||
constructor(
|
||||
private peer: Peer,
|
||||
knownStates: PeerKnownStates | undefined,
|
||||
) {
|
||||
this.optimisticKnownStates = knownStates?.clone() ?? new PeerKnownStates();
|
||||
this.knownStates = knownStates?.clone() ?? new PeerKnownStates();
|
||||
}
|
||||
|
||||
readonly optimisticKnownStates = new PeerKnownStates();
|
||||
/**
|
||||
* Here we to collect all the known states that a given peer has told us about.
|
||||
*
|
||||
* This can be used to safely track the sync state of a coValue in a given peer.
|
||||
*/
|
||||
readonly knownStates: PeerKnownStates;
|
||||
|
||||
/**
|
||||
* This one collects the known states "optimistically".
|
||||
* We use it to keep track of the content we have sent to a given peer.
|
||||
*
|
||||
* The main difference with knownState is that this is updated when the content is sent to the peer without
|
||||
* waiting for any acknowledgement from the peer.
|
||||
*/
|
||||
readonly optimisticKnownStates: PeerKnownStates;
|
||||
readonly toldKnownState: Set<RawCoID> = new Set();
|
||||
|
||||
get id() {
|
||||
|
||||
124
packages/cojson/src/SyncStateSubscriptionManager.ts
Normal file
124
packages/cojson/src/SyncStateSubscriptionManager.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { RawCoID } from "./ids.js";
|
||||
import {
|
||||
CoValueKnownState,
|
||||
PeerID,
|
||||
SyncManager,
|
||||
emptyKnownState,
|
||||
} from "./sync.js";
|
||||
|
||||
export class SyncStateSubscriptionManager {
|
||||
constructor(private syncManager: SyncManager) {}
|
||||
|
||||
private listeners = new Set<
|
||||
(
|
||||
peerId: PeerID,
|
||||
knownState: CoValueKnownState,
|
||||
uploadCompleted: boolean,
|
||||
) => void
|
||||
>();
|
||||
|
||||
private listenersByPeers = new Map<
|
||||
PeerID,
|
||||
Set<(knownState: CoValueKnownState, uploadCompleted: boolean) => void>
|
||||
>();
|
||||
|
||||
subscribeToUpdates(
|
||||
listener: (
|
||||
peerId: PeerID,
|
||||
knownState: CoValueKnownState,
|
||||
uploadCompleted: boolean,
|
||||
) => void,
|
||||
) {
|
||||
this.listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
subscribeToPeerUpdates(
|
||||
peerId: PeerID,
|
||||
listener: (knownState: CoValueKnownState, uploadCompleted: boolean) => void,
|
||||
) {
|
||||
const listeners = this.listenersByPeers.get(peerId) ?? new Set();
|
||||
|
||||
if (listeners.size === 0) {
|
||||
this.listenersByPeers.set(peerId, listeners);
|
||||
}
|
||||
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
triggerUpdate(peerId: PeerID, id: RawCoID) {
|
||||
const peer = this.syncManager.peers[peerId];
|
||||
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerListeners = this.listenersByPeers.get(peer.id);
|
||||
|
||||
// If we don't have any active listeners do nothing
|
||||
if (!peerListeners?.size && !this.listeners.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const knownState = peer.knownStates.get(id) ?? emptyKnownState(id);
|
||||
const fullyUploadedIntoPeer = this.getIsCoValueFullyUploadedIntoPeer(
|
||||
peerId,
|
||||
id,
|
||||
);
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
listener(peerId, knownState, fullyUploadedIntoPeer);
|
||||
}
|
||||
|
||||
if (!peerListeners) return;
|
||||
|
||||
for (const listener of peerListeners) {
|
||||
listener(knownState, fullyUploadedIntoPeer);
|
||||
}
|
||||
}
|
||||
|
||||
getIsCoValueFullyUploadedIntoPeer(peerId: PeerID, id: RawCoID) {
|
||||
const peer = this.syncManager.peers[peerId];
|
||||
const entry = this.syncManager.local.coValues[id];
|
||||
|
||||
if (!peer || !entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.state.type !== "available") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const coValue = entry.state.coValue;
|
||||
const knownState = peer.knownStates.get(id);
|
||||
|
||||
if (!knownState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getIsUploadCompleted(
|
||||
coValue.knownState().sessions,
|
||||
knownState.sessions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getIsUploadCompleted(
|
||||
from: Record<string, number>,
|
||||
to: Record<string, number>,
|
||||
) {
|
||||
for (const sessionId of Object.keys(from)) {
|
||||
if (from[sessionId] !== to[sessionId]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result, err, ok } from "neverthrow";
|
||||
import { AnyRawCoValue, RawCoValue } from "./coValue.js";
|
||||
import { AnyRawCoValue, CoID, RawCoValue } from "./coValue.js";
|
||||
import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
|
||||
import { RawGroup } from "./coValues/group.js";
|
||||
import { coreToCoValue } from "./coreToCoValue.js";
|
||||
@@ -785,6 +785,46 @@ export class CoValueCore {
|
||||
}
|
||||
}
|
||||
|
||||
// try to find revelation to parent group read keys
|
||||
|
||||
for (const co of content.keys()) {
|
||||
if (co.startsWith("parent_")) {
|
||||
const parentGroupID = co.slice("parent_".length) as CoID<RawGroup>;
|
||||
const parentGroup = this.node.expectCoValueLoaded(
|
||||
parentGroupID,
|
||||
"Expected parent group to be loaded",
|
||||
);
|
||||
|
||||
const parentKey = parentGroup.getCurrentReadKey();
|
||||
if (!parentKey.secret) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const revelationForParentKey = content.get(
|
||||
`${keyID}_for_${parentKey.id}`,
|
||||
);
|
||||
|
||||
if (revelationForParentKey) {
|
||||
const secret = parentGroup.crypto.decryptKeySecret(
|
||||
{
|
||||
encryptedID: keyID,
|
||||
encryptingID: parentKey.id,
|
||||
encrypted: revelationForParentKey,
|
||||
},
|
||||
parentKey.secret,
|
||||
);
|
||||
|
||||
if (secret) {
|
||||
return secret as KeySecret;
|
||||
} else {
|
||||
console.error(
|
||||
`Encrypting parent ${parentKey.id} key didn't decrypt ${keyID}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} else if (this.header.ruleset.type === "ownedByGroup") {
|
||||
return this.node
|
||||
@@ -950,9 +990,15 @@ export class CoValueCore {
|
||||
/** @internal */
|
||||
getDependedOnCoValuesUncached(): RawCoID[] {
|
||||
return this.header.ruleset.type === "group"
|
||||
? expectGroup(this.getCurrentContent())
|
||||
.keys()
|
||||
.filter((k): k is RawAccountID => k.startsWith("co_"))
|
||||
? [
|
||||
...expectGroup(this.getCurrentContent())
|
||||
.keys()
|
||||
.filter((k): k is RawAccountID => k.startsWith("co_")),
|
||||
...expectGroup(this.getCurrentContent())
|
||||
.keys()
|
||||
.filter((k) => k.startsWith("parent_"))
|
||||
.map((k) => k.replace("parent_", "") as RawCoID),
|
||||
]
|
||||
: this.header.ruleset.type === "ownedByGroup"
|
||||
? [
|
||||
this.header.ruleset.group,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Result, err, ok } from "neverthrow";
|
||||
import { Result, ok } from "neverthrow";
|
||||
import { CoID, RawCoValue } from "../coValue.js";
|
||||
import {
|
||||
CoValueCore,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
|
||||
import { AgentID, isAgentID } from "../ids.js";
|
||||
import { JsonObject } from "../jsonValue.js";
|
||||
import { Role } from "../permissions.js";
|
||||
import { expectGroup } from "../typeUtils/expectGroup.js";
|
||||
import {
|
||||
ControlledAccountOrAgent,
|
||||
RawAccount,
|
||||
@@ -29,6 +30,8 @@ export type GroupShape = {
|
||||
KeySecret,
|
||||
{ encryptedID: KeyID; encryptingID: KeyID }
|
||||
>;
|
||||
[parent: `parent_${CoID<RawGroup>}`]: "extend";
|
||||
[child: `child_${CoID<RawGroup>}`]: "extend";
|
||||
};
|
||||
|
||||
/** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
|
||||
@@ -61,12 +64,68 @@ export class RawGroup<
|
||||
* @category 1. Role reading
|
||||
*/
|
||||
roleOf(accountID: RawAccountID): Role | undefined {
|
||||
return this.roleOfInternal(accountID);
|
||||
return this.roleOfInternal(accountID)?.role;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
roleOfInternal(accountID: RawAccountID | AgentID): Role | undefined {
|
||||
return this.get(accountID);
|
||||
roleOfInternal(
|
||||
accountID: RawAccountID | AgentID | typeof EVERYONE,
|
||||
): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
|
||||
const roleHere = this.get(accountID);
|
||||
if (roleHere === "revoked") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let roleInfo:
|
||||
| {
|
||||
role: Exclude<Role, "revoked">;
|
||||
via: CoID<RawGroup> | undefined;
|
||||
}
|
||||
| undefined = roleHere && { role: roleHere, via: undefined };
|
||||
|
||||
const parentGroups = this.getParentGroups();
|
||||
|
||||
for (const parentGroup of parentGroups) {
|
||||
const roleInParent = parentGroup.roleOfInternal(accountID);
|
||||
|
||||
if (
|
||||
roleInParent &&
|
||||
roleInParent.role !== "revoked" &&
|
||||
isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
|
||||
) {
|
||||
roleInfo = { role: roleInParent.role, via: parentGroup.id };
|
||||
}
|
||||
}
|
||||
|
||||
return roleInfo;
|
||||
}
|
||||
|
||||
getParentGroups(): RawGroup[] {
|
||||
return (
|
||||
this.keys().filter((key) =>
|
||||
key.startsWith("parent_"),
|
||||
) as `parent_${CoID<RawGroup>}`[]
|
||||
).map((parentKey) => {
|
||||
const parent = this.core.node.expectCoValueLoaded(
|
||||
parentKey.slice("parent_".length) as CoID<RawGroup>,
|
||||
"Expected parent group to be loaded",
|
||||
);
|
||||
return expectGroup(parent.getCurrentContent());
|
||||
});
|
||||
}
|
||||
|
||||
getChildGroups(): RawGroup[] {
|
||||
return (
|
||||
this.keys().filter((key) =>
|
||||
key.startsWith("child_"),
|
||||
) as `child_${CoID<RawGroup>}`[]
|
||||
).map((childKey) => {
|
||||
const child = this.core.node.expectCoValueLoaded(
|
||||
childKey.slice("child_".length) as CoID<RawGroup>,
|
||||
"Expected child group to be loaded",
|
||||
);
|
||||
return expectGroup(child.getCurrentContent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +134,7 @@ export class RawGroup<
|
||||
* @category 1. Role reading
|
||||
*/
|
||||
myRole(): Role | undefined {
|
||||
return this.roleOfInternal(this.core.node.account.id);
|
||||
return this.roleOfInternal(this.core.node.account.id)?.role;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +262,75 @@ export class RawGroup<
|
||||
"trusting",
|
||||
);
|
||||
|
||||
console.log("Setting", `readKey`, "to", newReadKey.id, "in", this.id);
|
||||
|
||||
this.set("readKey", newReadKey.id, "trusting");
|
||||
|
||||
for (const parent of this.getParentGroups()) {
|
||||
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
||||
parent.core.getCurrentReadKey();
|
||||
if (!parentReadKeySecret) {
|
||||
throw new Error(
|
||||
"Can't reveal new child key to parent where we don't have access to the parent read key",
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Setting",
|
||||
`${newReadKey.id}_for_${parentReadKeyID}`,
|
||||
"in",
|
||||
this.id,
|
||||
);
|
||||
|
||||
this.set(
|
||||
`${newReadKey.id}_for_${parentReadKeyID}`,
|
||||
this.core.crypto.encryptKeySecret({
|
||||
encrypting: {
|
||||
id: parentReadKeyID,
|
||||
secret: parentReadKeySecret,
|
||||
},
|
||||
toEncrypt: newReadKey,
|
||||
}).encrypted,
|
||||
"trusting",
|
||||
);
|
||||
}
|
||||
|
||||
for (const child of this.getChildGroups()) {
|
||||
console.log("Rotating child", child.id);
|
||||
child.rotateReadKey();
|
||||
}
|
||||
}
|
||||
|
||||
extend(parent: RawGroup) {
|
||||
this.set(`parent_${parent.id}`, "extend", "trusting");
|
||||
parent.set(`child_${this.id}`, "extend", "trusting");
|
||||
|
||||
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
||||
parent.core.getCurrentReadKey();
|
||||
if (!parentReadKeySecret) {
|
||||
throw new Error("Can't extend group without parent read key secret");
|
||||
}
|
||||
|
||||
const { id: childReadKeyID, secret: childReadKeySecret } =
|
||||
this.core.getCurrentReadKey();
|
||||
if (!childReadKeySecret) {
|
||||
throw new Error("Can't extend group without child read key secret");
|
||||
}
|
||||
|
||||
this.set(
|
||||
`${childReadKeyID}_for_${parentReadKeyID}`,
|
||||
this.core.crypto.encryptKeySecret({
|
||||
encrypting: {
|
||||
id: parentReadKeyID,
|
||||
secret: parentReadKeySecret,
|
||||
},
|
||||
toEncrypt: {
|
||||
id: childReadKeyID,
|
||||
secret: childReadKeySecret,
|
||||
},
|
||||
}).encrypted,
|
||||
"trusting",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,6 +474,34 @@ export class RawGroup<
|
||||
}
|
||||
}
|
||||
|
||||
function isMorePermissiveAndShouldInherit(
|
||||
roleInParent: Role,
|
||||
roleInChild: Exclude<Role, "revoked"> | undefined,
|
||||
) {
|
||||
// invites should never be inherited
|
||||
if (
|
||||
roleInParent === "adminInvite" ||
|
||||
roleInParent === "writerInvite" ||
|
||||
roleInParent === "readerInvite"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (roleInParent === "admin") {
|
||||
return !roleInChild || roleInChild !== "admin";
|
||||
}
|
||||
|
||||
if (roleInParent === "writer") {
|
||||
return !roleInChild || roleInChild === "reader";
|
||||
}
|
||||
|
||||
if (roleInParent === "reader") {
|
||||
return !roleInChild;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export type InviteSecret = `inviteSecret_z${string}`;
|
||||
|
||||
function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CoID } from "./coValue.js";
|
||||
import { CoValueCore, Transaction } from "./coValueCore.js";
|
||||
import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
|
||||
import { MapOpPayload } from "./coValues/coMap.js";
|
||||
import { EVERYONE, Everyone } from "./coValues/group.js";
|
||||
import { EVERYONE, Everyone, RawGroup } from "./coValues/group.js";
|
||||
import { KeyID } from "./crypto/crypto.js";
|
||||
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
||||
import { parseJSON } from "./jsonStringify.js";
|
||||
@@ -28,198 +28,12 @@ export function determineValidTransactions(
|
||||
coValue: CoValueCore,
|
||||
): { txID: TransactionID; tx: Transaction }[] {
|
||||
if (coValue.header.ruleset.type === "group") {
|
||||
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
|
||||
([sessionID, sessionLog]) => {
|
||||
return sessionLog.transactions.map((tx, txIndex) => ({
|
||||
sessionID,
|
||||
txIndex,
|
||||
tx,
|
||||
})) as {
|
||||
sessionID: SessionID;
|
||||
txIndex: number;
|
||||
tx: Transaction;
|
||||
}[];
|
||||
},
|
||||
);
|
||||
|
||||
allTransactionsSorted.sort((a, b) => {
|
||||
return a.tx.madeAt - b.tx.madeAt;
|
||||
});
|
||||
|
||||
const initialAdmin = coValue.header.ruleset.initialAdmin;
|
||||
|
||||
if (!initialAdmin) {
|
||||
throw new Error("Group must have initialAdmin");
|
||||
}
|
||||
|
||||
const memberState: {
|
||||
[agent: RawAccountID | AgentID]: Role;
|
||||
[EVERYONE]?: Role;
|
||||
} = {};
|
||||
|
||||
const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
|
||||
|
||||
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
||||
// console.log("before", { memberState, validTransactions });
|
||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||
|
||||
if (tx.privacy === "private") {
|
||||
if (memberState[transactor] === "admin") {
|
||||
validTransactions.push({
|
||||
txID: { sessionID, txIndex },
|
||||
tx,
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
console.warn("Only admins can make private transactions in groups");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let changes;
|
||||
|
||||
try {
|
||||
changes = parseJSON(tx.changes);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
coValue.id,
|
||||
"Invalid JSON in transaction",
|
||||
e,
|
||||
tx,
|
||||
JSON.stringify(tx.changes, (k, v) =>
|
||||
k === "changes" || k === "encryptedChanges"
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const change = changes[0] as
|
||||
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
|
||||
| MapOpPayload<"readKey", JsonValue>
|
||||
| MapOpPayload<"profile", CoID<RawProfile>>;
|
||||
if (changes.length !== 1) {
|
||||
console.warn("Group transaction must have exactly one change");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.op !== "set") {
|
||||
console.warn("Group transaction must set a role or readKey");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.key === "readKey") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set readKeys");
|
||||
continue;
|
||||
}
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (change.key === "profile") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set profile");
|
||||
continue;
|
||||
}
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (
|
||||
isKeyForKeyField(change.key) ||
|
||||
isKeyForAccountField(change.key)
|
||||
) {
|
||||
if (
|
||||
memberState[transactor] !== "admin" &&
|
||||
memberState[transactor] !== "adminInvite" &&
|
||||
memberState[transactor] !== "writerInvite" &&
|
||||
memberState[transactor] !== "readerInvite"
|
||||
) {
|
||||
console.warn("Only admins can reveal keys");
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: check validity of agents who the key is revealed to?
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
}
|
||||
|
||||
const affectedMember = change.key;
|
||||
const assignedRole = change.value;
|
||||
|
||||
if (
|
||||
change.value !== "admin" &&
|
||||
change.value !== "writer" &&
|
||||
change.value !== "reader" &&
|
||||
change.value !== "revoked" &&
|
||||
change.value !== "adminInvite" &&
|
||||
change.value !== "writerInvite" &&
|
||||
change.value !== "readerInvite"
|
||||
) {
|
||||
console.warn("Group transaction must set a valid role");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
affectedMember === EVERYONE &&
|
||||
!(
|
||||
change.value === "reader" ||
|
||||
change.value === "writer" ||
|
||||
change.value === "revoked"
|
||||
)
|
||||
) {
|
||||
console.warn("Everyone can only be set to reader, writer or revoked");
|
||||
continue;
|
||||
}
|
||||
|
||||
const isFirstSelfAppointment =
|
||||
!memberState[transactor] &&
|
||||
transactor === initialAdmin &&
|
||||
change.op === "set" &&
|
||||
change.key === transactor &&
|
||||
change.value === "admin";
|
||||
|
||||
if (!isFirstSelfAppointment) {
|
||||
if (memberState[transactor] === "admin") {
|
||||
if (
|
||||
memberState[affectedMember] === "admin" &&
|
||||
affectedMember !== transactor &&
|
||||
assignedRole !== "admin"
|
||||
) {
|
||||
console.warn("Admins can only demote themselves.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "adminInvite") {
|
||||
if (change.value !== "admin") {
|
||||
console.warn("AdminInvites can only create admins.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "writerInvite") {
|
||||
if (change.value !== "writer") {
|
||||
console.warn("WriterInvites can only create writers.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "readerInvite") {
|
||||
if (change.value !== "reader") {
|
||||
console.warn("ReaderInvites can only create reader.");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Group transaction must be made by current admin or invite",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
memberState[affectedMember] = change.value;
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
|
||||
// console.log("after", { memberState, validTransactions });
|
||||
}
|
||||
|
||||
return validTransactions;
|
||||
return determineValidTransactionsForGroup(coValue, initialAdmin);
|
||||
} else if (coValue.header.ruleset.type === "ownedByGroup") {
|
||||
const groupContent = expectGroup(
|
||||
coValue.node
|
||||
@@ -241,27 +55,18 @@ export function determineValidTransactions(
|
||||
return sessionLog.transactions
|
||||
.filter((tx) => {
|
||||
const groupAtTime = groupContent.atTime(tx.madeAt);
|
||||
const effectiveTransactor =
|
||||
transactor === groupContent.id &&
|
||||
groupAtTime instanceof RawAccount
|
||||
? groupAtTime.currentAgentID().match(
|
||||
(agentID) => agentID,
|
||||
(e) => {
|
||||
console.error(
|
||||
"Error while determining current agent ID in valid transactions",
|
||||
e,
|
||||
);
|
||||
return undefined;
|
||||
},
|
||||
)
|
||||
: transactor;
|
||||
const effectiveTransactor = agentInAccountOrMemberInGroup(
|
||||
transactor,
|
||||
groupAtTime,
|
||||
);
|
||||
|
||||
if (!effectiveTransactor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transactorRoleAtTxTime =
|
||||
groupAtTime.get(effectiveTransactor) || groupAtTime.get(EVERYONE);
|
||||
groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
|
||||
groupAtTime.roleOfInternal(EVERYONE)?.role;
|
||||
|
||||
return (
|
||||
transactorRoleAtTxTime === "admin" ||
|
||||
@@ -291,6 +96,234 @@ export function determineValidTransactions(
|
||||
}
|
||||
}
|
||||
|
||||
function determineValidTransactionsForGroup(
|
||||
coValue: CoValueCore,
|
||||
initialAdmin: RawAccountID | AgentID,
|
||||
): { txID: TransactionID; tx: Transaction }[] {
|
||||
const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
|
||||
([sessionID, sessionLog]) => {
|
||||
return sessionLog.transactions.map((tx, txIndex) => ({
|
||||
sessionID,
|
||||
txIndex,
|
||||
tx,
|
||||
})) as {
|
||||
sessionID: SessionID;
|
||||
txIndex: number;
|
||||
tx: Transaction;
|
||||
}[];
|
||||
},
|
||||
);
|
||||
|
||||
allTransactionsSorted.sort((a, b) => {
|
||||
return a.tx.madeAt - b.tx.madeAt;
|
||||
});
|
||||
|
||||
const memberState: {
|
||||
[agent: RawAccountID | AgentID]: Role;
|
||||
[EVERYONE]?: Role;
|
||||
} = {};
|
||||
|
||||
const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
|
||||
|
||||
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
||||
// console.log("before", { memberState, validTransactions });
|
||||
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
||||
|
||||
if (tx.privacy === "private") {
|
||||
if (memberState[transactor] === "admin") {
|
||||
validTransactions.push({
|
||||
txID: { sessionID, txIndex },
|
||||
tx,
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
console.warn("Only admins can make private transactions in groups");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let changes;
|
||||
|
||||
try {
|
||||
changes = parseJSON(tx.changes);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
coValue.id,
|
||||
"Invalid JSON in transaction",
|
||||
e,
|
||||
tx,
|
||||
JSON.stringify(tx.changes, (k, v) =>
|
||||
k === "changes" || k === "encryptedChanges"
|
||||
? v.slice(0, 20) + "..."
|
||||
: v,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const change = changes[0] as
|
||||
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
|
||||
| MapOpPayload<"readKey", JsonValue>
|
||||
| MapOpPayload<"profile", CoID<RawProfile>>
|
||||
| MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
|
||||
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
|
||||
|
||||
if (changes.length !== 1) {
|
||||
console.warn("Group transaction must have exactly one change");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.op !== "set") {
|
||||
console.warn("Group transaction must set a role or readKey");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.key === "readKey") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set readKeys");
|
||||
continue;
|
||||
}
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (change.key === "profile") {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set profile");
|
||||
continue;
|
||||
}
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (
|
||||
isKeyForKeyField(change.key) ||
|
||||
isKeyForAccountField(change.key)
|
||||
) {
|
||||
if (
|
||||
memberState[transactor] !== "admin" &&
|
||||
memberState[transactor] !== "adminInvite" &&
|
||||
memberState[transactor] !== "writerInvite" &&
|
||||
memberState[transactor] !== "readerInvite"
|
||||
) {
|
||||
console.warn("Only admins can reveal keys");
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: check validity of agents who the key is revealed to?
|
||||
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (isParentExtension(change.key)) {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set parent extensions");
|
||||
continue;
|
||||
}
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
} else if (isChildExtension(change.key)) {
|
||||
if (memberState[transactor] !== "admin") {
|
||||
console.warn("Only admins can set child extensions");
|
||||
continue;
|
||||
}
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
continue;
|
||||
}
|
||||
|
||||
const affectedMember = change.key;
|
||||
const assignedRole = change.value;
|
||||
|
||||
if (
|
||||
change.value !== "admin" &&
|
||||
change.value !== "writer" &&
|
||||
change.value !== "reader" &&
|
||||
change.value !== "revoked" &&
|
||||
change.value !== "adminInvite" &&
|
||||
change.value !== "writerInvite" &&
|
||||
change.value !== "readerInvite"
|
||||
) {
|
||||
console.warn("Group transaction must set a valid role");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
affectedMember === EVERYONE &&
|
||||
!(
|
||||
change.value === "reader" ||
|
||||
change.value === "writer" ||
|
||||
change.value === "revoked"
|
||||
)
|
||||
) {
|
||||
console.warn("Everyone can only be set to reader, writer or revoked");
|
||||
continue;
|
||||
}
|
||||
|
||||
const isFirstSelfAppointment =
|
||||
!memberState[transactor] &&
|
||||
transactor === initialAdmin &&
|
||||
change.op === "set" &&
|
||||
change.key === transactor &&
|
||||
change.value === "admin";
|
||||
|
||||
if (!isFirstSelfAppointment) {
|
||||
if (memberState[transactor] === "admin") {
|
||||
if (
|
||||
memberState[affectedMember] === "admin" &&
|
||||
affectedMember !== transactor &&
|
||||
assignedRole !== "admin"
|
||||
) {
|
||||
console.warn("Admins can only demote themselves.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "adminInvite") {
|
||||
if (change.value !== "admin") {
|
||||
console.warn("AdminInvites can only create admins.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "writerInvite") {
|
||||
if (change.value !== "writer") {
|
||||
console.warn("WriterInvites can only create writers.");
|
||||
continue;
|
||||
}
|
||||
} else if (memberState[transactor] === "readerInvite") {
|
||||
if (change.value !== "reader") {
|
||||
console.warn("ReaderInvites can only create reader.");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Group transaction must be made by current admin or invite",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
memberState[affectedMember] = change.value;
|
||||
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
||||
|
||||
// console.log("after", { memberState, validTransactions });
|
||||
}
|
||||
|
||||
return validTransactions;
|
||||
}
|
||||
|
||||
function agentInAccountOrMemberInGroup(
|
||||
transactor: RawAccountID | AgentID,
|
||||
groupAtTime: RawGroup,
|
||||
): RawAccountID | AgentID | undefined {
|
||||
if (transactor === groupAtTime.id && groupAtTime instanceof RawAccount) {
|
||||
return groupAtTime.currentAgentID().match(
|
||||
(agentID) => agentID,
|
||||
(e) => {
|
||||
console.error(
|
||||
"Error while determining current agent ID in valid transactions",
|
||||
e,
|
||||
);
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
}
|
||||
return transactor;
|
||||
}
|
||||
|
||||
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
|
||||
return co.startsWith("key_") && co.includes("_for_key");
|
||||
}
|
||||
@@ -304,3 +337,11 @@ export function isKeyForAccountField(
|
||||
co.includes("_for_everyone")
|
||||
);
|
||||
}
|
||||
|
||||
function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
|
||||
return key.startsWith("parent_");
|
||||
}
|
||||
|
||||
function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
|
||||
return key.startsWith("child_");
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
asDependencyOf || id,
|
||||
);
|
||||
} else if (!known?.header && coValue.header?.ruleset.type === "group") {
|
||||
const dependedOnAccounts = new Set();
|
||||
const dependedOnAccountsAndGroups = new Set();
|
||||
for (const session of Object.values(coValue.sessionEntries)) {
|
||||
for (const entry of session) {
|
||||
for (const tx of entry.transactions) {
|
||||
@@ -154,16 +154,24 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
||||
const parsedChanges = JSON.parse(tx.changes);
|
||||
for (const change of parsedChanges) {
|
||||
if (change.op === "set" && change.key.startsWith("co_")) {
|
||||
dependedOnAccounts.add(change.key);
|
||||
dependedOnAccountsAndGroups.add(change.key);
|
||||
}
|
||||
if (
|
||||
change.op === "set" &&
|
||||
change.key.startsWith("parent_co_")
|
||||
) {
|
||||
dependedOnAccountsAndGroups.add(
|
||||
change.key.replace("parent_", ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const account of dependedOnAccounts) {
|
||||
for (const accountOrGroup of dependedOnAccountsAndGroups) {
|
||||
await this.sendNewContent(
|
||||
account as CoID<RawCoValue>,
|
||||
accountOrGroup as CoID<RawCoValue>,
|
||||
undefined,
|
||||
asDependencyOf || id,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PeerState } from "./PeerState.js";
|
||||
import { SyncStateSubscriptionManager } from "./SyncStateSubscriptionManager.js";
|
||||
import { CoValueHeader, Transaction } from "./coValueCore.js";
|
||||
import { CoValueCore } from "./coValueCore.js";
|
||||
import { CoValueState } from "./coValueState.js";
|
||||
@@ -116,8 +117,11 @@ export class SyncManager {
|
||||
|
||||
constructor(local: LocalNode) {
|
||||
this.local = local;
|
||||
this.syncStateSubscriptionManager = new SyncStateSubscriptionManager(this);
|
||||
}
|
||||
|
||||
syncStateSubscriptionManager: SyncStateSubscriptionManager;
|
||||
|
||||
peersInPriorityOrder(): PeerState[] {
|
||||
return Object.values(this.peers).sort((a, b) => {
|
||||
const aPriority = a.priority || 0;
|
||||
@@ -127,6 +131,10 @@ export class SyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
getPeers(): PeerState[] {
|
||||
return Object.values(this.peers);
|
||||
}
|
||||
|
||||
async loadFromPeers(id: RawCoID, forPeer?: PeerID) {
|
||||
const eligiblePeers = this.peersInPriorityOrder().filter(
|
||||
(peer) => peer.id !== forPeer && peer.isServerOrStoragePeer(),
|
||||
@@ -298,9 +306,20 @@ export class SyncManager {
|
||||
}
|
||||
|
||||
addPeer(peer: Peer) {
|
||||
const peerState = new PeerState(peer);
|
||||
const prevPeer = this.peers[peer.id];
|
||||
const peerState = new PeerState(peer, prevPeer?.knownStates);
|
||||
this.peers[peer.id] = peerState;
|
||||
|
||||
if (prevPeer && !prevPeer.closed) {
|
||||
prevPeer.gracefulShutdown();
|
||||
}
|
||||
|
||||
const unsubscribeFromKnownStatesUpdates = peerState.knownStates.subscribe(
|
||||
(id) => {
|
||||
this.syncStateSubscriptionManager.triggerUpdate(peer.id, id);
|
||||
},
|
||||
);
|
||||
|
||||
if (peerState.isServerOrStoragePeer()) {
|
||||
const initialSync = async () => {
|
||||
for (const id of Object.keys(this.local.coValues) as RawCoID[]) {
|
||||
@@ -358,8 +377,9 @@ export class SyncManager {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
peer.outgoing.close();
|
||||
delete this.peers[peer.id];
|
||||
const state = this.peers[peer.id];
|
||||
state?.gracefulShutdown();
|
||||
unsubscribeFromKnownStatesUpdates();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -455,6 +475,12 @@ export class SyncManager {
|
||||
value: knownStateIn(msg),
|
||||
});
|
||||
|
||||
peer.knownStates.dispatch({
|
||||
type: "COMBINE_WITH",
|
||||
id: msg.id,
|
||||
value: knownStateIn(msg),
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
if (msg.asDependencyOf) {
|
||||
if (this.local.coValues[msg.asDependencyOf]) {
|
||||
@@ -616,8 +642,6 @@ export class SyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
await this.syncCoValue(coValue);
|
||||
|
||||
if (invalidStateAssumed) {
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
@@ -626,7 +650,28 @@ export class SyncManager {
|
||||
}).catch((e) => {
|
||||
console.error("Error sending known state correction", e);
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* We are sending a known state message to the peer to acknowledge the
|
||||
* receipt of the new content.
|
||||
*
|
||||
* This way the sender knows that the content has been received and applied
|
||||
* and can update their peer's knownState accordingly.
|
||||
*/
|
||||
this.trySendToPeer(peer, {
|
||||
action: "known",
|
||||
...coValue.knownState(),
|
||||
}).catch((e: unknown) => {
|
||||
console.error("Error sending known state", e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We do send a correction/ack message before syncing to give an immediate
|
||||
* response to the peers that are waiting for confirmation that a coValue is
|
||||
* fully synced
|
||||
*/
|
||||
await this.syncCoValue(coValue);
|
||||
}
|
||||
|
||||
async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
|
||||
@@ -670,6 +715,7 @@ export class SyncManager {
|
||||
async actuallySyncCoValue(coValue: CoValueCore) {
|
||||
// let blockingSince = performance.now();
|
||||
for (const peer of this.peersInPriorityOrder()) {
|
||||
if (peer.closed) continue;
|
||||
// if (performance.now() - blockingSince > 5) {
|
||||
// await new Promise<void>((resolve) => {
|
||||
// setTimeout(resolve, 0);
|
||||
@@ -684,6 +730,35 @@ export class SyncManager {
|
||||
await this.sendNewContentIncludingDependencies(coValue.id, peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const peer of this.getPeers()) {
|
||||
this.syncStateSubscriptionManager.triggerUpdate(peer.id, coValue.id);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForUploadIntoPeer(peerId: PeerID, id: RawCoID) {
|
||||
const isAlreadyUploaded =
|
||||
this.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
peerId,
|
||||
id,
|
||||
);
|
||||
|
||||
if (isAlreadyUploaded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe =
|
||||
this.syncStateSubscriptionManager.subscribeToPeerUpdates(
|
||||
peerId,
|
||||
(knownState, uploadCompleted) => {
|
||||
if (uploadCompleted && knownState.id === id) {
|
||||
resolve(true);
|
||||
unsubscribe?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
gracefulShutdown() {
|
||||
|
||||
232
packages/cojson/src/tests/SyncStateSubscriptionManager.test.ts
Normal file
232
packages/cojson/src/tests/SyncStateSubscriptionManager.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { describe, expect, onTestFinished, test, vi } from "vitest";
|
||||
import { connectedPeers } from "../streamUtils.js";
|
||||
import { emptyKnownState } from "../sync.js";
|
||||
import { createTestNode, waitFor } from "./testUtils.js";
|
||||
|
||||
describe("SyncStateSubscriptionManager", () => {
|
||||
test("subscribeToUpdates receives updates when peer state changes", async () => {
|
||||
// Setup nodes
|
||||
const client = createTestNode();
|
||||
const jazzCloud = createTestNode();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
// Connect nodes
|
||||
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
|
||||
"clientConnection",
|
||||
"jazzCloudConnection",
|
||||
{
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
},
|
||||
);
|
||||
|
||||
client.syncManager.addPeer(jazzCloudAsPeer);
|
||||
jazzCloud.syncManager.addPeer(clientAsPeer);
|
||||
|
||||
const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
|
||||
|
||||
const updateSpy = vi.fn();
|
||||
const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledWith(
|
||||
"jazzCloudConnection",
|
||||
emptyKnownState(map.core.id),
|
||||
false,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledWith(
|
||||
"jazzCloudConnection",
|
||||
client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
|
||||
map.core.id,
|
||||
)!,
|
||||
true,
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test("subscribeToPeerUpdates receives updates only for specific peer", async () => {
|
||||
// Setup nodes
|
||||
const client = createTestNode();
|
||||
const jazzCloud = createTestNode();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
// Connect nodes
|
||||
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
|
||||
"clientConnection",
|
||||
"jazzCloudConnection",
|
||||
{
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
},
|
||||
);
|
||||
|
||||
const [clientStoragePeer] = connectedPeers("clientStorage", "unusedPeer", {
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
});
|
||||
|
||||
client.syncManager.addPeer(jazzCloudAsPeer);
|
||||
client.syncManager.addPeer(clientStoragePeer);
|
||||
jazzCloud.syncManager.addPeer(clientAsPeer);
|
||||
|
||||
const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
|
||||
|
||||
const updateToJazzCloudSpy = vi.fn();
|
||||
const updateToStorageSpy = vi.fn();
|
||||
const unsubscribe1 = subscriptionManager.subscribeToPeerUpdates(
|
||||
"jazzCloudConnection",
|
||||
updateToJazzCloudSpy,
|
||||
);
|
||||
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
|
||||
"clientStorage",
|
||||
updateToStorageSpy,
|
||||
);
|
||||
|
||||
onTestFinished(() => {
|
||||
unsubscribe1();
|
||||
unsubscribe2();
|
||||
});
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
|
||||
emptyKnownState(map.core.id),
|
||||
false,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
expect(updateToJazzCloudSpy).toHaveBeenLastCalledWith(
|
||||
client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
|
||||
map.core.id,
|
||||
)!,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(updateToStorageSpy).toHaveBeenLastCalledWith(
|
||||
emptyKnownState(map.core.id),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("getIsCoValueFullyUploadedIntoPeer returns correct status", async () => {
|
||||
// Setup nodes
|
||||
const client = createTestNode();
|
||||
const jazzCloud = createTestNode();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
// Connect nodes
|
||||
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
|
||||
"clientConnection",
|
||||
"jazzCloudConnection",
|
||||
{
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
},
|
||||
);
|
||||
|
||||
client.syncManager.addPeer(jazzCloudAsPeer);
|
||||
jazzCloud.syncManager.addPeer(clientAsPeer);
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
|
||||
|
||||
expect(
|
||||
subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
await waitFor(() => {
|
||||
return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("unsubscribe stops receiving updates", async () => {
|
||||
// Setup nodes
|
||||
const client = createTestNode();
|
||||
const jazzCloud = createTestNode();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
// Connect nodes
|
||||
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
|
||||
"clientConnection",
|
||||
"jazzCloudConnection",
|
||||
{
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
},
|
||||
);
|
||||
|
||||
client.syncManager.addPeer(jazzCloudAsPeer);
|
||||
jazzCloud.syncManager.addPeer(clientAsPeer);
|
||||
|
||||
const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
|
||||
const anyUpdateSpy = vi.fn();
|
||||
const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
|
||||
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
|
||||
"jazzCloudConnection",
|
||||
anyUpdateSpy,
|
||||
);
|
||||
|
||||
unsubscribe1();
|
||||
unsubscribe2();
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
anyUpdateSpy.mockClear();
|
||||
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
expect(anyUpdateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1708,3 +1708,751 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
|
||||
childContent2.set("foo", "bar2", "private");
|
||||
expect(childContent2.get("foo")).toEqual("bar2");
|
||||
});
|
||||
|
||||
test("Admins can set parent extensions", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
||||
});
|
||||
|
||||
test("Writers, readers and invitees can not set parent extensions", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
const adminInvite = node.createAccount();
|
||||
const writerInvite = node.createAccount();
|
||||
const readerInvite = node.createAccount();
|
||||
|
||||
group.addMember(writer, "writer");
|
||||
group.addMember(reader, "reader");
|
||||
group.addMember(adminInvite, "adminInvite");
|
||||
group.addMember(writerInvite, "writerInvite");
|
||||
group.addMember(readerInvite, "readerInvite");
|
||||
|
||||
const groupAsWriter = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriter.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsWriter.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsReader = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReader.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsReader.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsAdminInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
adminInvite,
|
||||
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsAdminInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsAdminInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsWriterInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
writerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
writerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriterInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsWriterInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsReaderInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
readerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
readerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReaderInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsReaderInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Admins can set child extensions", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const childGroup = node.createGroup();
|
||||
|
||||
group.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(group.get(`child_${childGroup.id}`)).toEqual("extend");
|
||||
});
|
||||
|
||||
test("Writers, readers and invitees can not set child extensions", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const childGroup = node.createGroup();
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
const adminInvite = node.createAccount();
|
||||
const writerInvite = node.createAccount();
|
||||
const readerInvite = node.createAccount();
|
||||
|
||||
group.addMember(writer, "writer");
|
||||
group.addMember(reader, "reader");
|
||||
group.addMember(adminInvite, "adminInvite");
|
||||
group.addMember(writerInvite, "writerInvite");
|
||||
group.addMember(readerInvite, "readerInvite");
|
||||
|
||||
const groupAsWriter = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriter.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsWriter.get(`child_${childGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsReader = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReader.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsReader.get(`child_${childGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsAdminInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
adminInvite,
|
||||
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsAdminInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsAdminInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsWriterInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
writerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
writerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriterInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsWriterInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
||||
|
||||
const groupAsReaderInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
readerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
readerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReaderInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
||||
expect(groupAsReaderInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Member roles are inherited by child groups (except invites)", () => {
|
||||
const { group, node, admin } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
const adminInvite = node.createAccount();
|
||||
const writerInvite = node.createAccount();
|
||||
const readerInvite = node.createAccount();
|
||||
|
||||
parentGroup.addMember(writer, "writer");
|
||||
parentGroup.addMember(reader, "reader");
|
||||
parentGroup.addMember(adminInvite, "adminInvite");
|
||||
parentGroup.addMember(writerInvite, "writerInvite");
|
||||
parentGroup.addMember(readerInvite, "readerInvite");
|
||||
|
||||
expect(group.roleOfInternal(admin.id)).toEqual({
|
||||
role: "admin",
|
||||
via: undefined,
|
||||
});
|
||||
|
||||
expect(group.roleOfInternal(writer.id)).toEqual({
|
||||
role: "writer",
|
||||
via: parentGroup.id,
|
||||
});
|
||||
expect(group.roleOf(writer.id)).toEqual("writer");
|
||||
|
||||
expect(group.roleOfInternal(reader.id)).toEqual({
|
||||
role: "reader",
|
||||
via: parentGroup.id,
|
||||
});
|
||||
expect(group.roleOf(reader.id)).toEqual("reader");
|
||||
|
||||
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
|
||||
|
||||
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
|
||||
|
||||
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Member roles are inherited by grand-children groups (except invites)", () => {
|
||||
const { group, node, admin } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
const grandParentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
const adminInvite = node.createAccount();
|
||||
const writerInvite = node.createAccount();
|
||||
const readerInvite = node.createAccount();
|
||||
|
||||
grandParentGroup.addMember(writer, "writer");
|
||||
grandParentGroup.addMember(reader, "reader");
|
||||
grandParentGroup.addMember(adminInvite, "adminInvite");
|
||||
grandParentGroup.addMember(writerInvite, "writerInvite");
|
||||
grandParentGroup.addMember(readerInvite, "readerInvite");
|
||||
|
||||
expect(group.roleOfInternal(admin.id)).toEqual({
|
||||
role: "admin",
|
||||
via: undefined,
|
||||
});
|
||||
|
||||
expect(group.roleOfInternal(writer.id)).toEqual({
|
||||
role: "writer",
|
||||
via: parentGroup.id,
|
||||
});
|
||||
expect(group.roleOf(writer.id)).toEqual("writer");
|
||||
|
||||
expect(group.roleOfInternal(reader.id)).toEqual({
|
||||
role: "reader",
|
||||
via: parentGroup.id,
|
||||
});
|
||||
expect(group.roleOf(reader.id)).toEqual("reader");
|
||||
|
||||
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
|
||||
|
||||
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
|
||||
|
||||
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
|
||||
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("Admins can reveal parent read keys to child groups", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
if (!parentReadKeyID) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const readKeyID = group.get("readKey");
|
||||
if (!readKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const encrypted = "fake_encrypted_key_secret" as any;
|
||||
|
||||
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
||||
expect(group.get(`${readKeyID}_for_${parentReadKeyID}`)).toEqual(encrypted);
|
||||
});
|
||||
|
||||
test("Writers, readers and invites can't reveal parent read keys to child groups", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
if (!parentReadKeyID) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const readKeyID = group.get("readKey");
|
||||
if (!readKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const encrypted = "fake_encrypted_key_secret" as any;
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
const adminInvite = node.createAccount();
|
||||
const writerInvite = node.createAccount();
|
||||
const readerInvite = node.createAccount();
|
||||
|
||||
group.addMember(writer, "writer");
|
||||
group.addMember(reader, "reader");
|
||||
group.addMember(adminInvite, "adminInvite");
|
||||
group.addMember(writerInvite, "writerInvite");
|
||||
group.addMember(readerInvite, "readerInvite");
|
||||
|
||||
const groupAsWriter = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriter.set(
|
||||
`${readKeyID}_for_${parentReadKeyID}`,
|
||||
encrypted,
|
||||
"trusting",
|
||||
);
|
||||
expect(
|
||||
groupAsWriter.get(`${readKeyID}_for_${parentReadKeyID}`),
|
||||
).toBeUndefined();
|
||||
|
||||
const groupAsReader = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReader.set(
|
||||
`${readKeyID}_for_${parentReadKeyID}`,
|
||||
encrypted,
|
||||
"trusting",
|
||||
);
|
||||
expect(
|
||||
groupAsReader.get(`${readKeyID}_for_${parentReadKeyID}`),
|
||||
).toBeUndefined();
|
||||
|
||||
const groupAsAdminInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
adminInvite,
|
||||
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsAdminInvite.set(
|
||||
`${readKeyID}_for_${parentReadKeyID}`,
|
||||
encrypted,
|
||||
"trusting",
|
||||
);
|
||||
expect(
|
||||
groupAsAdminInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
||||
).toBeUndefined();
|
||||
|
||||
const groupAsWriterInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
writerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
writerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsWriterInvite.set(
|
||||
`${readKeyID}_for_${parentReadKeyID}`,
|
||||
encrypted,
|
||||
"trusting",
|
||||
);
|
||||
expect(
|
||||
groupAsWriterInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
||||
).toBeUndefined();
|
||||
|
||||
const groupAsReaderInvite = expectGroup(
|
||||
group.core
|
||||
.testWithDifferentAccount(
|
||||
readerInvite,
|
||||
Crypto.newRandomSessionID(
|
||||
readerInvite.currentAgentID()._unsafeUnwrap(),
|
||||
),
|
||||
)
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
groupAsReaderInvite.set(
|
||||
`${readKeyID}_for_${parentReadKeyID}`,
|
||||
encrypted,
|
||||
"trusting",
|
||||
);
|
||||
expect(
|
||||
groupAsReaderInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Writers and readers in a parent group can read from an object owned by a child group", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
const parentKey =
|
||||
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
|
||||
if (!parentReadKeyID || !parentKey) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const readKeyID = group.get("readKey");
|
||||
const readKey = readKeyID && group.core.getReadKey(readKeyID);
|
||||
if (!readKeyID || !readKey) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
const encrypted = node.crypto.encryptKeySecret({
|
||||
toEncrypt: {
|
||||
id: readKeyID,
|
||||
secret: readKey,
|
||||
},
|
||||
encrypting: {
|
||||
id: parentReadKeyID,
|
||||
secret: parentKey,
|
||||
},
|
||||
}).encrypted;
|
||||
|
||||
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
||||
|
||||
const writer = node.createAccount();
|
||||
const reader = node.createAccount();
|
||||
parentGroup.addMember(writer, "writer");
|
||||
parentGroup.addMember(reader, "reader");
|
||||
|
||||
const childObject = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const childContent = expectMap(childObject.getCurrentContent());
|
||||
|
||||
childContent.set("foo", "bar", "private");
|
||||
expect(childContent.get("foo")).toEqual("bar");
|
||||
|
||||
const childContentAsWriter = expectMap(
|
||||
childObject
|
||||
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(childContentAsWriter.get("foo")).toEqual("bar");
|
||||
|
||||
const childContentAsReader = expectMap(
|
||||
childObject
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(childContentAsReader.get("foo")).toEqual("bar");
|
||||
});
|
||||
|
||||
test("Writers in a parent group can write to an object owned by a child group", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
const parentKey =
|
||||
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
|
||||
if (!parentReadKeyID || !parentKey) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const readKeyID = group.get("readKey");
|
||||
const readKey = readKeyID && group.core.getReadKey(readKeyID);
|
||||
if (!readKeyID || !readKey) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
const encrypted = node.crypto.encryptKeySecret({
|
||||
toEncrypt: {
|
||||
id: readKeyID,
|
||||
secret: readKey,
|
||||
},
|
||||
encrypting: {
|
||||
id: parentReadKeyID,
|
||||
secret: parentKey,
|
||||
},
|
||||
}).encrypted;
|
||||
|
||||
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
||||
|
||||
const writer = node.createAccount();
|
||||
parentGroup.addMember(writer, "writer");
|
||||
|
||||
const childObject = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const childContentAsWriter = expectMap(
|
||||
childObject
|
||||
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
childContentAsWriter.set("foo", "bar", "private");
|
||||
expect(childContentAsWriter.get("foo")).toEqual("bar");
|
||||
});
|
||||
|
||||
test("When rotating the key of a child group, the new child key is exposed to the parent group", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const currentReadKeyID = group.get("readKey");
|
||||
if (!currentReadKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
group.rotateReadKey();
|
||||
|
||||
const newReadKeyID = group.get("readKey");
|
||||
if (!newReadKeyID) {
|
||||
throw new Error("Can't get new group read key");
|
||||
}
|
||||
expect(newReadKeyID).not.toEqual(currentReadKeyID);
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
if (!parentReadKeyID) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
console.log("Checking", `${newReadKeyID}_for_${parentReadKeyID}`);
|
||||
|
||||
expect(group.get(`${newReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
|
||||
});
|
||||
|
||||
test("When rotating the key of a parent group, the keys of all child groups are also rotated", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
parentGroup.set(`child_${group.id}`, "extend", "trusting");
|
||||
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
||||
|
||||
group.rotateReadKey();
|
||||
|
||||
const currentChildReadKeyID = group.get("readKey");
|
||||
if (!currentChildReadKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
console.log("child id", group.id);
|
||||
parentGroup.rotateReadKey();
|
||||
|
||||
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
|
||||
"readKey",
|
||||
);
|
||||
if (!newChildReadKeyID) {
|
||||
throw new Error("Can't get new group read key");
|
||||
}
|
||||
|
||||
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
|
||||
});
|
||||
|
||||
test("When rotating the key of a grand-parent group, the keys of all child and grand-child groups are also rotated", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const grandParentGroup = node.createGroup();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
grandParentGroup.set(`child_${parentGroup.id}`, "extend", "trusting");
|
||||
parentGroup.set(`child_${group.id}`, "extend", "trusting");
|
||||
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
||||
group.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
||||
|
||||
const currentGrandParentReadKeyID = grandParentGroup.get("readKey");
|
||||
if (!currentGrandParentReadKeyID) {
|
||||
throw new Error("Can't get grand-parent group read key");
|
||||
}
|
||||
|
||||
const currentParentReadKeyID = parentGroup.get("readKey");
|
||||
if (!currentParentReadKeyID) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const currentChildReadKeyID = group.get("readKey");
|
||||
if (!currentChildReadKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
grandParentGroup.rotateReadKey();
|
||||
|
||||
const newGrandParentReadKeyID = grandParentGroup.get("readKey");
|
||||
if (!newGrandParentReadKeyID) {
|
||||
throw new Error("Can't get new grand-parent group read key");
|
||||
}
|
||||
|
||||
expect(newGrandParentReadKeyID).not.toEqual(currentGrandParentReadKeyID);
|
||||
|
||||
const newParentReadKeyID = expectGroup(
|
||||
parentGroup.core.getCurrentContent(),
|
||||
).get("readKey");
|
||||
if (!newParentReadKeyID) {
|
||||
throw new Error("Can't get new parent group read key");
|
||||
}
|
||||
|
||||
expect(newParentReadKeyID).not.toEqual(currentParentReadKeyID);
|
||||
|
||||
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
|
||||
"readKey",
|
||||
);
|
||||
if (!newChildReadKeyID) {
|
||||
throw new Error("Can't get new group read key");
|
||||
}
|
||||
|
||||
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
|
||||
});
|
||||
|
||||
test("Calling extend on group sets up parent and child references and reveals child key to parent", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.extend(parentGroup);
|
||||
|
||||
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
||||
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
|
||||
|
||||
const parentReadKeyID = parentGroup.get("readKey");
|
||||
if (!parentReadKeyID) {
|
||||
throw new Error("Can't get parent group read key");
|
||||
}
|
||||
|
||||
const childReadKeyID = group.get("readKey");
|
||||
if (!childReadKeyID) {
|
||||
throw new Error("Can't get group read key");
|
||||
}
|
||||
|
||||
expect(group.get(`${childReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
|
||||
|
||||
const reader = node.createAccount();
|
||||
parentGroup.addMember(reader, "reader");
|
||||
|
||||
const childObject = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
const childMap = expectMap(childObject.getCurrentContent());
|
||||
|
||||
childMap.set("foo", "bar", "private");
|
||||
|
||||
const childContentAsReader = expectMap(
|
||||
childObject
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(childContentAsReader.get("foo")).toEqual("bar");
|
||||
});
|
||||
|
||||
test("Calling extend to create grand-child groups parent and child references and reveals child key to parent(s)", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
const grandParentGroup = node.createGroup();
|
||||
|
||||
group.extend(parentGroup);
|
||||
parentGroup.extend(grandParentGroup);
|
||||
|
||||
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
||||
expect(parentGroup.get(`parent_${grandParentGroup.id}`)).toEqual("extend");
|
||||
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
|
||||
expect(grandParentGroup.get(`child_${parentGroup.id}`)).toEqual("extend");
|
||||
|
||||
const reader = node.createAccount();
|
||||
grandParentGroup.addMember(reader, "reader");
|
||||
|
||||
const childObject = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
const childMap = expectMap(childObject.getCurrentContent());
|
||||
|
||||
childMap.set("foo", "bar", "private");
|
||||
|
||||
const childContentAsReader = expectMap(
|
||||
childObject
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(childContentAsReader.get("foo")).toEqual("bar");
|
||||
});
|
||||
|
||||
test("High-level permissions work correctly when a group is extended", () => {
|
||||
const { group, node } = newGroupHighLevel();
|
||||
const parentGroup = node.createGroup();
|
||||
|
||||
group.extend(parentGroup);
|
||||
|
||||
const reader = node.createAccount();
|
||||
parentGroup.addMember(reader, "reader");
|
||||
|
||||
const mapCore = node.createCoValue({
|
||||
type: "comap",
|
||||
ruleset: { type: "ownedByGroup", group: group.id },
|
||||
meta: null,
|
||||
...Crypto.createdNowUnique(),
|
||||
});
|
||||
|
||||
const map = expectMap(mapCore.getCurrentContent());
|
||||
|
||||
map.set("foo", "bar", "private");
|
||||
|
||||
const mapAsReader = expectMap(
|
||||
mapCore
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(mapAsReader.get("foo")).toEqual("bar");
|
||||
|
||||
const groupKeyBeforeRemove = group.core.getCurrentReadKey().id;
|
||||
|
||||
parentGroup.removeMember(reader);
|
||||
|
||||
const groupKeyAfterRemove = group.core.getCurrentReadKey().id;
|
||||
expect(groupKeyAfterRemove).not.toEqual(groupKeyBeforeRemove);
|
||||
|
||||
map.set("foo", "baz", "private");
|
||||
|
||||
const mapAsReaderAfterRemove = expectMap(
|
||||
mapCore
|
||||
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
||||
.getCurrentContent(),
|
||||
);
|
||||
|
||||
expect(mapAsReaderAfterRemove.get("foo")).not.toEqual("baz");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { expectMap } from "../coValue.js";
|
||||
import { CoValueHeader } from "../coValueCore.js";
|
||||
import { RawAccountID } from "../coValues/account.js";
|
||||
@@ -10,7 +10,11 @@ import { LocalNode } from "../localNode.js";
|
||||
import { getPriorityFromHeader } from "../priority.js";
|
||||
import { connectedPeers, newQueuePair } from "../streamUtils.js";
|
||||
import { SyncMessage } from "../sync.js";
|
||||
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
||||
import {
|
||||
createTestNode,
|
||||
randomAnonymousAccountAndSessionID,
|
||||
waitFor,
|
||||
} from "./testUtils.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -1561,6 +1565,303 @@ describe("sync - extra tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function createTwoConnectedNodes() {
|
||||
// Setup nodes
|
||||
const client = createTestNode();
|
||||
const jazzCloud = createTestNode();
|
||||
|
||||
// Connect nodes initially
|
||||
const [connectionWithClientAsPeer, jazzCloudConnectionAsPeer] =
|
||||
connectedPeers("connectionWithClient", "jazzCloudConnection", {
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
});
|
||||
|
||||
client.syncManager.addPeer(jazzCloudConnectionAsPeer);
|
||||
jazzCloud.syncManager.addPeer(connectionWithClientAsPeer);
|
||||
|
||||
return {
|
||||
client,
|
||||
jazzCloud,
|
||||
connectionWithClientAsPeer,
|
||||
jazzCloudConnectionAsPeer,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SyncManager - knownStates vs optimisticKnownStates", () => {
|
||||
test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
|
||||
const { client } = createTwoConnectedNodes();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
// Wait for the full sync to complete
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
const peerState = client.syncManager.peers["jazzCloudConnection"]!;
|
||||
|
||||
// The optimisticKnownStates should be the same as the knownStates after the full sync is complete
|
||||
expect(peerState.optimisticKnownStates.get(map.core.id)).toEqual(
|
||||
peerState.knownStates.get(map.core.id),
|
||||
);
|
||||
});
|
||||
|
||||
test("optimisticKnownStates is updated as new transactions are received, while knownStates only when the coValue is fully synced", async () => {
|
||||
const { client, jazzCloudConnectionAsPeer } = createTwoConnectedNodes();
|
||||
|
||||
// Create test data and sync the first change
|
||||
// We want that both the nodes know about the coValue so we can test
|
||||
// the content acknowledgement flow.
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
// Block the content messages
|
||||
// The main difference between optimisticKnownStates and knownStates is that
|
||||
// optimisticKnownStates is updated when the content messages are sent,
|
||||
// while knownStates is only updated when we receive the "known" messages
|
||||
// that are acknowledging the receipt of the content messages
|
||||
const push = jazzCloudConnectionAsPeer.outgoing.push;
|
||||
const pushSpy = vi.spyOn(jazzCloudConnectionAsPeer.outgoing, "push");
|
||||
|
||||
const blockedMessages: SyncMessage[] = [];
|
||||
|
||||
pushSpy.mockImplementation(async (msg) => {
|
||||
if (msg.action === "content") {
|
||||
blockedMessages.push(msg);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return push.call(jazzCloudConnectionAsPeer.outgoing, msg);
|
||||
});
|
||||
|
||||
map.set("key2", "value2", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
const peerState = client.syncManager.peers["jazzCloudConnection"]!;
|
||||
|
||||
expect(peerState.optimisticKnownStates.get(map.core.id)).not.toEqual(
|
||||
peerState.knownStates.get(map.core.id),
|
||||
);
|
||||
|
||||
// Restore the implementation of push and send the blocked messages
|
||||
// After this the full sync can be completed and the other node will
|
||||
// respond with a "known" message acknowledging the receipt of the content messages
|
||||
pushSpy.mockRestore();
|
||||
|
||||
for (const msg of blockedMessages) {
|
||||
await jazzCloudConnectionAsPeer.outgoing.push(msg);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
expect(peerState.optimisticKnownStates.get(map.core.id)).toEqual(
|
||||
peerState.knownStates.get(map.core.id),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SyncManager.addPeer", () => {
|
||||
test("new peer gets a copy of previous peer's knownStates when replacing it", async () => {
|
||||
const { client } = createTwoConnectedNodes();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
// Wait for initial sync
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
// Store the initial known states
|
||||
const initialKnownStates =
|
||||
client.syncManager.peers["jazzCloudConnection"]!.knownStates;
|
||||
|
||||
// Create new connection with same ID
|
||||
const [jazzCloudConnectionAsPeer2] = connectedPeers(
|
||||
"jazzCloudConnection",
|
||||
"unusedPeer",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
|
||||
// Add new peer with same ID
|
||||
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
|
||||
|
||||
// Verify that the new peer has a copy of the previous known states
|
||||
const newPeerKnownStates =
|
||||
client.syncManager.peers["jazzCloudConnection"]!.knownStates;
|
||||
|
||||
expect(newPeerKnownStates).not.toBe(initialKnownStates); // Should be a different instance
|
||||
expect(newPeerKnownStates.get(map.core.id)).toEqual(
|
||||
initialKnownStates.get(map.core.id),
|
||||
);
|
||||
});
|
||||
|
||||
test("new peer with new ID starts with empty knownStates", async () => {
|
||||
const { client } = createTwoConnectedNodes();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
// Wait for initial sync
|
||||
await waitFor(() => {
|
||||
return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
|
||||
"jazzCloudConnection",
|
||||
map.core.id,
|
||||
);
|
||||
});
|
||||
|
||||
// Connect second peer with different ID
|
||||
const [brandNewPeer] = connectedPeers("brandNewPeer", "unusedPeer", {
|
||||
peer1role: "client",
|
||||
peer2role: "server",
|
||||
});
|
||||
|
||||
// Add new peer with different ID
|
||||
client.syncManager.addPeer(brandNewPeer);
|
||||
|
||||
// Verify that the new peer starts with empty known states
|
||||
const newPeerKnownStates =
|
||||
client.syncManager.peers["brandNewPeer"]!.knownStates;
|
||||
expect(newPeerKnownStates.get(map.core.id)).toBe(undefined);
|
||||
});
|
||||
|
||||
test("when adding a peer with the same ID as a previous peer, the previous peer is closed", async () => {
|
||||
const { client } = createTwoConnectedNodes();
|
||||
|
||||
// Store reference to first peer
|
||||
const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
|
||||
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
|
||||
|
||||
// Create and add replacement peer
|
||||
const [jazzCloudConnectionAsPeer2] = connectedPeers(
|
||||
"jazzCloudConnection",
|
||||
"unusedPeer",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
|
||||
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
|
||||
|
||||
// Verify thet the first peer had ben closed correctly
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
expect(firstPeer.closed).toBe(true);
|
||||
});
|
||||
|
||||
test("when adding a peer with the same ID as a previous peer and the previous peer is closed, do not attempt to close it again", async () => {
|
||||
const { client } = createTwoConnectedNodes();
|
||||
|
||||
// Store reference to first peer
|
||||
const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
|
||||
|
||||
firstPeer.gracefulShutdown();
|
||||
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
|
||||
|
||||
// Create and add replacement peer
|
||||
const [jazzCloudConnectionAsPeer2] = connectedPeers(
|
||||
"jazzCloudConnection",
|
||||
"unusedPeer",
|
||||
{
|
||||
peer1role: "server",
|
||||
peer2role: "client",
|
||||
},
|
||||
);
|
||||
|
||||
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
|
||||
|
||||
// Verify thet the first peer had not been closed again
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
expect(firstPeer.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("waitForUploadIntoPeer", () => {
|
||||
test("should resolve when the coValue is fully uploaded into the peer", async () => {
|
||||
const { client, jazzCloudConnectionAsPeer: peer } =
|
||||
createTwoConnectedNodes();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
await expect(
|
||||
Promise.race([
|
||||
client.syncManager.waitForUploadIntoPeer(peer.id, map.core.id),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), 100),
|
||||
),
|
||||
]),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test("should not resolve when the coValue is not synced", async () => {
|
||||
const { client, jazzCloudConnectionAsPeer: peer } =
|
||||
createTwoConnectedNodes();
|
||||
|
||||
// Create test data
|
||||
const group = client.createGroup();
|
||||
const map = group.createMap();
|
||||
map.set("key1", "value1", "trusting");
|
||||
|
||||
vi.spyOn(peer.outgoing, "push").mockImplementation(async () => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await client.syncManager.actuallySyncCoValue(map.core);
|
||||
|
||||
await expect(
|
||||
Promise.race([
|
||||
client.syncManager.waitForUploadIntoPeer(peer.id, map.core.id),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), 100),
|
||||
),
|
||||
]),
|
||||
).rejects.toThrow("Timeout");
|
||||
});
|
||||
});
|
||||
|
||||
function groupContentEx(group: RawGroup) {
|
||||
return {
|
||||
action: "content",
|
||||
|
||||
@@ -18,6 +18,11 @@ export function randomAnonymousAccountAndSessionID(): [
|
||||
return [new ControlledAgent(agentSecret, Crypto), sessionID];
|
||||
}
|
||||
|
||||
export function createTestNode() {
|
||||
const [admin, session] = randomAnonymousAccountAndSessionID();
|
||||
return new LocalNode(admin, session, Crypto);
|
||||
}
|
||||
|
||||
export function newGroup() {
|
||||
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
|
||||
|
||||
@@ -93,3 +98,31 @@ export function shouldNotResolve<T>(
|
||||
setTimeout(resolve, ops.timeout);
|
||||
});
|
||||
}
|
||||
|
||||
export function waitFor(callback: () => boolean | void) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkPassed = () => {
|
||||
try {
|
||||
return { ok: callback(), error: null };
|
||||
} catch (error) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
let retries = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const { ok, error } = checkPassed();
|
||||
|
||||
if (ok !== false) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (++retries > 10) {
|
||||
clearInterval(interval);
|
||||
reject(error);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-browser@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-browser@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-browser@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "jazz-browser-auth-clerk",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"jazz-browser": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16"
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0"
|
||||
},
|
||||
"scripts": {
|
||||
"format-and-lint": "biome check .",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- jazz-browser@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-browser-media-images",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -8,8 +8,8 @@
|
||||
"dependencies": {
|
||||
"@types/image-blob-reduce": "^4.1.1",
|
||||
"image-blob-reduce": "^4.1.0",
|
||||
"jazz-browser": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"pica": "^9.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson-storage-indexeddb@0.8.19-group-inheritance.0
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- cojson-transport-ws@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- cojson-storage-indexeddb@0.8.18
|
||||
- cojson-transport-ws@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- Updated dependencies [b6162f0]
|
||||
- cojson@0.8.17
|
||||
- cojson-transport-ws@0.8.17
|
||||
- cojson-storage-indexeddb@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "jazz-browser",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^1.3.0",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson-storage-indexeddb": "workspace:0.8.16",
|
||||
"cojson-transport-ws": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-storage-indexeddb": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# jazz-autosub
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- cojson-transport-ws@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- cojson-transport-ws@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- Updated dependencies [b6162f0]
|
||||
- cojson@0.8.17
|
||||
- cojson-transport-ws@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"types": "src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson-transport-ws": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-browser-auth-clerk@0.8.19-group-inheritance.0
|
||||
- jazz-react@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-browser-auth-clerk@0.8.18
|
||||
- jazz-react@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-browser-auth-clerk@0.8.17
|
||||
- jazz-react@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "jazz-react-auth-clerk",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:0.8.16",
|
||||
"jazz-browser-auth-clerk": "workspace:0.8.16",
|
||||
"jazz-react": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16"
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-browser-auth-clerk": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-react": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# jazz-browser-media-images
|
||||
|
||||
## 0.8.15-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-native-media-images",
|
||||
"version": "0.8.12",
|
||||
"version": "0.8.15-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# jazz-browser
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- cojson-transport-ws@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- cojson-transport-ws@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- Updated dependencies [b6162f0]
|
||||
- cojson@0.8.17
|
||||
- cojson-transport-ws@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-native",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# jazz-react
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-browser@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-browser@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-browser@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "jazz-react",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^1.3.0",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"jazz-browser": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-browser": "workspace:0.8.19-group-inheritance.0",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# jazz-run
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson-storage-sqlite@0.8.19-group-inheritance.0
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- cojson-transport-ws@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- cojson-storage-sqlite@0.8.18
|
||||
- cojson-transport-ws@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d433cf4: Improve the sync wait using the new API from the sync manager
|
||||
- Updated dependencies [d433cf4]
|
||||
- Updated dependencies [b6162f0]
|
||||
- cojson@0.8.17
|
||||
- cojson-transport-ws@0.8.17
|
||||
- cojson-storage-sqlite@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"bin": "./dist/index.js",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"scripts": {
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
@@ -18,11 +18,11 @@
|
||||
"@effect/printer-ansi": "^0.34.5",
|
||||
"@effect/schema": "^0.71.1",
|
||||
"@effect/typeclass": "^0.25.5",
|
||||
"cojson": "workspace:0.8.16",
|
||||
"cojson-storage-sqlite": "workspace:0.8.16",
|
||||
"cojson-transport-ws": "workspace:0.8.16",
|
||||
"cojson": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-storage-sqlite": "workspace:0.8.19-group-inheritance.0",
|
||||
"cojson-transport-ws": "workspace:0.8.19-group-inheritance.0",
|
||||
"effect": "^3.6.5",
|
||||
"jazz-tools": "workspace:0.8.16",
|
||||
"jazz-tools": "workspace:0.8.19-group-inheritance.0",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -47,8 +47,8 @@ export const createWorkerAccount = async ({
|
||||
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
waitForSync(account, peer, accountCoValue),
|
||||
waitForSync(account, peer, accountProfileCoValue),
|
||||
syncManager.waitForUploadIntoPeer(peer.id, accountCoValue.id),
|
||||
syncManager.waitForUploadIntoPeer(peer.id, accountProfileCoValue.id),
|
||||
]),
|
||||
failAfter(
|
||||
4_000,
|
||||
@@ -73,7 +73,7 @@ export const createWorkerAccount = async ({
|
||||
peersToLoadFrom: [peer2],
|
||||
crypto,
|
||||
}),
|
||||
failAfter(4_000, "Timeout: Account loading check failed"),
|
||||
failAfter(10_000, "Timeout: Account loading check failed"),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -82,62 +82,6 @@ export const createWorkerAccount = async ({
|
||||
};
|
||||
};
|
||||
|
||||
function waitForSync(account: Account, peer: Peer, coValue: CoValueCore) {
|
||||
const syncManager = account._raw.core.node.syncManager;
|
||||
const peerState = syncManager.peers[peer.id];
|
||||
|
||||
if (!peerState) {
|
||||
throw new Error(`Peer state for ${peer.id} not found`);
|
||||
}
|
||||
|
||||
const isSynced = () => {
|
||||
const knownState = coValue.knownState();
|
||||
|
||||
if (!peerState.optimisticKnownStates.get(coValue.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isEqualSession(
|
||||
knownState.sessions,
|
||||
peerState.optimisticKnownStates.get(coValue.id)?.sessions ?? {},
|
||||
);
|
||||
};
|
||||
|
||||
if (isSynced()) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe = peerState?.optimisticKnownStates.subscribe(
|
||||
(id, knownState) => {
|
||||
if (id !== coValue.id) return;
|
||||
|
||||
if (isSynced()) {
|
||||
resolve(true);
|
||||
unsubscribe?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function isEqualSession(a: Record<string, number>, b: Record<string, number>) {
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const sessionId of keysA) {
|
||||
if (a[sessionId] !== b[sessionId]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function failAfter(ms: number, errorMessage: string) {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(errorMessage)), ms);
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-tools
|
||||
|
||||
## 0.8.19-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8b87117: Implement Group Inheritance
|
||||
- Updated dependencies [8b87117]
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
|
||||
## 0.8.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
|
||||
## 0.8.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.8.16",
|
||||
"version": "0.8.19-group-inheritance.0",
|
||||
"dependencies": {
|
||||
"cojson": "workspace:*",
|
||||
"fast-check": "^3.17.2"
|
||||
|
||||
@@ -142,6 +142,11 @@ export class Group extends CoValueBase implements CoValue {
|
||||
return this;
|
||||
}
|
||||
|
||||
removeMember(member: Everyone | Account) {
|
||||
this._raw.removeMember(member === "everyone" ? member : member._raw);
|
||||
return this;
|
||||
}
|
||||
|
||||
get members() {
|
||||
return this._raw
|
||||
.keys()
|
||||
@@ -172,6 +177,11 @@ export class Group extends CoValueBase implements CoValue {
|
||||
});
|
||||
}
|
||||
|
||||
extend(parent: Group) {
|
||||
this._raw.extend(parent._raw);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @category Subscription & Loading */
|
||||
static load<G extends Group, Depth>(
|
||||
this: CoValueClass<G>,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RawGroup } from "cojson";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { Account, CoMap, Group, WasmCrypto, co } from "../index.web.js";
|
||||
|
||||
@@ -85,3 +86,95 @@ describe("Custom accounts and groups", async () => {
|
||||
expect(map._owner.castAs(CustomGroup).nMembers).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Group inheritance", () => {
|
||||
class TestMap extends CoMap {
|
||||
title = co.string;
|
||||
}
|
||||
|
||||
test("Group inheritance", async () => {
|
||||
const me = await Account.create({
|
||||
creationProps: { name: "Hermes Puggington" },
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const parentGroup = Group.create({ owner: me });
|
||||
const group = Group.create({ owner: me });
|
||||
|
||||
group.extend(parentGroup);
|
||||
|
||||
console.log(
|
||||
group.id,
|
||||
group._raw.core.getDependedOnCoValuesUncached(),
|
||||
parentGroup.id,
|
||||
);
|
||||
|
||||
console.log(
|
||||
(group._raw.core.getCurrentContent() as RawGroup)
|
||||
.keys()
|
||||
.filter((k) => k.startsWith("parent_"))
|
||||
.map((k) => k.replace("parent_", "")),
|
||||
);
|
||||
|
||||
const reader = await Account.createAs(me, {
|
||||
creationProps: { name: "Reader" },
|
||||
});
|
||||
|
||||
parentGroup.addMember(reader, "reader");
|
||||
|
||||
const mapInChild = TestMap.create({ title: "In Child" }, { owner: group });
|
||||
|
||||
const mapAsReader = await TestMap.load(mapInChild.id, reader, {});
|
||||
expect(mapAsReader?.title).toBe("In Child");
|
||||
|
||||
parentGroup.removeMember(reader);
|
||||
|
||||
mapInChild.title = "In Child (updated)";
|
||||
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(
|
||||
mapInChild.id,
|
||||
reader,
|
||||
{},
|
||||
);
|
||||
expect(mapAsReaderAfterUpdate?.title).toBe("In Child");
|
||||
});
|
||||
|
||||
test("Group inheritance with grand-children", async () => {
|
||||
const me = await Account.create({
|
||||
creationProps: { name: "Hermes Puggington" },
|
||||
crypto: Crypto,
|
||||
});
|
||||
|
||||
const grandParentGroup = Group.create({ owner: me });
|
||||
const parentGroup = Group.create({ owner: me });
|
||||
const group = Group.create({ owner: me });
|
||||
|
||||
group.extend(parentGroup);
|
||||
parentGroup.extend(grandParentGroup);
|
||||
|
||||
const reader = await Account.createAs(me, {
|
||||
creationProps: { name: "Reader" },
|
||||
});
|
||||
|
||||
grandParentGroup.addMember(reader, "reader");
|
||||
|
||||
const mapInGrandChild = TestMap.create(
|
||||
{ title: "In Grand Child" },
|
||||
{ owner: group },
|
||||
);
|
||||
|
||||
const mapAsReader = await TestMap.load(mapInGrandChild.id, reader, {});
|
||||
expect(mapAsReader?.title).toBe("In Grand Child");
|
||||
|
||||
grandParentGroup.removeMember(reader);
|
||||
|
||||
mapInGrandChild.title = "In Grand Child (updated)";
|
||||
|
||||
const mapAsReaderAfterUpdate = await TestMap.load(
|
||||
mapInGrandChild.id,
|
||||
reader,
|
||||
{},
|
||||
);
|
||||
expect(mapAsReaderAfterUpdate?.title).toBe("In Grand Child");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# jazz-react
|
||||
|
||||
## 0.8.9-group-inheritance.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b87117]
|
||||
- jazz-tools@0.8.19-group-inheritance.0
|
||||
- cojson@0.8.19-group-inheritance.0
|
||||
- jazz-browser@0.8.19-group-inheritance.0
|
||||
|
||||
## 0.8.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d4319d8]
|
||||
- cojson@0.8.18
|
||||
- jazz-browser@0.8.18
|
||||
- jazz-tools@0.8.18
|
||||
|
||||
## 0.8.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d433cf4]
|
||||
- cojson@0.8.17
|
||||
- jazz-browser@0.8.17
|
||||
- jazz-tools@0.8.17
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-vue",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.9-group-inheritance.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
102
pnpm-lock.yaml
generated
102
pnpm-lock.yaml
generated
@@ -46,7 +46,7 @@ importers:
|
||||
e2e/BinaryCoStream:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/cojson
|
||||
hash-slash:
|
||||
specifier: workspace:0.2.1
|
||||
@@ -55,10 +55,10 @@ importers:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
@@ -150,13 +150,13 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
jazz-browser-media-images:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-browser-media-images
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
next:
|
||||
specifier: 14.2.5
|
||||
@@ -205,16 +205,16 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/cojson
|
||||
hash-slash:
|
||||
specifier: workspace:0.2.1
|
||||
version: link:../../packages/hash-slash
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
lucide-react:
|
||||
specifier: ^0.274.0
|
||||
@@ -302,19 +302,19 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/cojson
|
||||
hash-slash:
|
||||
specifier: workspace:0.2.1
|
||||
version: link:../../packages/hash-slash
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-react-auth-clerk:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react-auth-clerk
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
lucide-react:
|
||||
specifier: ^0.274.0
|
||||
@@ -738,10 +738,10 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/cojson
|
||||
cojson-transport-ws:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/cojson-transport-ws
|
||||
hash-slash:
|
||||
specifier: workspace:0.2.1
|
||||
@@ -823,10 +823,10 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
lucide-react:
|
||||
specifier: ^0.274.0
|
||||
@@ -881,10 +881,10 @@ importers:
|
||||
examples/password-manager:
|
||||
dependencies:
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
@@ -945,13 +945,13 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
jazz-browser-media-images:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-browser-media-images
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
lucide-react:
|
||||
specifier: ^0.274.0
|
||||
@@ -1006,7 +1006,7 @@ importers:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
jazz-run:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-run
|
||||
postcss:
|
||||
specifier: ^8.4.27
|
||||
@@ -1044,10 +1044,10 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../../packages/jazz-tools
|
||||
lucide-react:
|
||||
specifier: ^0.274.0
|
||||
@@ -1145,7 +1145,7 @@ importers:
|
||||
packages/cojson-storage-indexeddb:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -1170,7 +1170,7 @@ importers:
|
||||
specifier: ^8.5.2
|
||||
version: 8.7.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -1183,7 +1183,7 @@ importers:
|
||||
packages/cojson-transport-ws:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -1211,16 +1211,16 @@ importers:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
cojson-storage-indexeddb:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson-storage-indexeddb
|
||||
cojson-transport-ws:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson-transport-ws
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -1229,13 +1229,13 @@ importers:
|
||||
packages/jazz-browser-auth-clerk:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
jazz-browser:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-browser
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
devDependencies:
|
||||
typescript:
|
||||
@@ -1251,10 +1251,10 @@ importers:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
jazz-browser:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-browser
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
pica:
|
||||
specifier: ^9.0.1
|
||||
@@ -1270,13 +1270,13 @@ importers:
|
||||
packages/jazz-nodejs:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
cojson-transport-ws:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson-transport-ws
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
ws:
|
||||
specifier: ^8.14.2
|
||||
@@ -1295,13 +1295,13 @@ importers:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
jazz-browser:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-browser
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
@@ -1320,16 +1320,16 @@ importers:
|
||||
packages/jazz-react-auth-clerk:
|
||||
dependencies:
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
jazz-browser-auth-clerk:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-browser-auth-clerk
|
||||
jazz-react:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-react
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
@@ -1413,19 +1413,19 @@ importers:
|
||||
specifier: ^0.25.5
|
||||
version: 0.25.5(effect@3.6.5)
|
||||
cojson:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson
|
||||
cojson-storage-sqlite:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson-storage-sqlite
|
||||
cojson-transport-ws:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../cojson-transport-ws
|
||||
effect:
|
||||
specifier: ^3.6.5
|
||||
version: 3.6.5
|
||||
jazz-tools:
|
||||
specifier: workspace:0.8.16
|
||||
specifier: workspace:0.8.18
|
||||
version: link:../jazz-tools
|
||||
ws:
|
||||
specifier: ^8.14.2
|
||||
|
||||
Reference in New Issue
Block a user