Compare commits

...

32 Commits

Author SHA1 Message Date
Anselm
dd8dba63ea Fix migration changes being lost on loaded account 2024-01-02 18:36:18 +00:00
Anselm Eickhoff
219071654d Merge pull request #153 from tobiaslins/move-to-localStorage 2023-12-21 07:43:36 +00:00
Tobias Lins
7c415db7bd Move to auth to use localStorage instead of sessionStorage 2023-12-21 08:28:03 +01:00
Anselm
a4b484fa36 Publish 2023-12-20 21:49:54 +00:00
Anselm
3757d12dc4 Fix loading of accounts 2023-12-20 21:48:01 +00:00
Anselm
fc5b670c73 Publish 2023-12-20 11:33:39 +00:00
Anselm
c8adcc4c47 Fix unused var 2023-12-20 11:32:40 +00:00
Anselm
41a755fe41 publish 2023-12-20 11:17:25 +00:00
Anselm
8def1bb29e IndexedDB & timer perf improvements 2023-12-20 11:04:45 +00:00
Anselm
d379b04e33 Publish 2023-12-18 15:15:27 +00:00
Anselm
17a30e054e Add passphrase based auth + example 2023-12-18 15:13:37 +00:00
Anselm
7d8f4b4c00 Publish jazz-browser-auth0 2023-12-05 19:12:16 +00:00
Anselm
e2a3896bf0 v0.0.0 2023-12-05 19:11:00 +00:00
Anselm
446de8e0ff Fix for not receiving user_metadata 2023-12-05 19:10:15 +00:00
Anselm
5ae6c95878 Use authorization params everywhere 2023-12-05 17:07:18 +00:00
Anselm
7cde349a50 Actually deploy auth0 example 2023-12-05 16:45:41 +00:00
Anselm
61e640f574 Fix auth0 example dependencies 2023-12-05 16:43:18 +00:00
Anselm
ed122d9d8e Publish 2023-12-05 16:41:12 +00:00
Anselm
34817f4536 Auth0 integration and example 2023-12-05 16:38:37 +00:00
Anselm
0998a0eabf Correct homepage path 2023-11-24 17:09:34 +00:00
Anselm
a96108478b Homepage updates 2023-11-24 17:04:03 +00:00
Anselm
a4cf4c40d4 publish 2023-11-08 11:42:36 +00:00
Anselm Eickhoff
934fe4d29b Merge pull request #127 from KyleAMathews/main
fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts
2023-11-07 17:18:02 +00:00
Anselm Eickhoff
408012f2e5 remove redundant peer add 2023-11-07 17:17:41 +00:00
Kyle Mathews
d0078b830e fix(jazz-nodejs): also set peersToLoadFrom on newly created accounts 2023-11-03 14:58:43 -07:00
Anselm Eickhoff
e52948b2b7 Merge pull request #125 from gardencmp/jazz-nodejs
jazz-nodejs MVP
2023-11-02 12:11:56 +00:00
Anselm
53bb1b230b Fix interpolation 2023-11-02 11:53:19 +00:00
Anselm
54e83aeaaa use NOMAD_ADDR secret 2023-11-02 11:43:32 +00:00
Anselm
aa3129cab5 Provide localNode to AccountMigrations 2023-10-27 14:48:36 +01:00
Anselm
90520dddd7 Make addMember and removeMember take loaded Accounts instead of just IDs 2023-10-27 14:30:55 +01:00
Anselm
03eb77070a Allow account migrations to be async 2023-10-27 11:18:41 +01:00
Anselm
4ba5c255b6 Fewer assumptions in jazz-nodejs 2023-10-27 11:00:19 +01:00
136 changed files with 4339 additions and 893 deletions

View File

@@ -11,8 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
steps:
- uses: actions/checkout@v3
@@ -54,40 +53,39 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
# build-homepage:
# runs-on: ubuntu-latest
build-homepage:
runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
steps:
- uses: actions/checkout@v3
with:
submodules: true
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: gardencmp
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: gardencmp
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Docker Build & Push
# uses: docker/build-push-action@v4
# with:
# context: ./homepage/homepage-jazz
# push: true
# tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
# cache-from: type=gha
# cache-to: type=gha,mode=max
- name: Docker Build & Push
uses: docker/build-push-action@v4
with:
context: ./homepage
push: true
tags: ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-examples:
runs-on: ubuntu-latest
needs: build-examples
strategy:
matrix:
# example: ["chat", "todo", "pets", "twit", "file-drop"]
example: ["twit", "chat"]
example: ["twit", "chat", "counter-js-auth0", "pets", "twit", "file-drop"]
steps:
- uses: actions/checkout@v3
@@ -115,38 +113,38 @@ jobs:
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./examples/${{ matrix.example }}
# deploy-homepage:
# runs-on: ubuntu-latest
# needs: build-homepage
deploy-homepage:
runs-on: ubuntu-latest
needs: build-homepage
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: true
# - uses: gacts/install-nomad@v1
# - name: Tailscale
# uses: tailscale/github-action@v1
# with:
# authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: gacts/install-nomad@v1
- name: Tailscale
uses: tailscale/github-action@v1
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
# - name: Deploy on Nomad
# run: |
# if [ "${{github.ref_name}}" == "main" ]; then
# export BRANCH_SUFFIX="";
# export BRANCH_SUBDOMAIN="";
# else
# export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
# export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
# fi
- name: Deploy on Nomad
run: |
if [ "${{github.ref_name}}" == "main" ]; then
export BRANCH_SUFFIX="";
export BRANCH_SUBDOMAIN="";
else
export BRANCH_SUFFIX=-${{github.head_ref || github.ref_name}};
export BRANCH_SUBDOMAIN=${{github.head_ref || github.ref_name}}.;
fi
# export DOCKER_USER=gardencmp;
# export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
# export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
export DOCKER_USER=gardencmp;
export DOCKER_PASSWORD=${{ secrets.DOCKER_PULL_PAT }};
export DOCKER_TAG=ghcr.io/gardencmp/${{github.event.repository.name}}-homepage-jazz:${{github.head_ref || github.ref_name}}-${{github.sha}}-${{github.run_number}}-${{github.run_attempt}};
# envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
# cat job-instance.nomad;
# NOMAD_ADDR='http://control1v2-london:4646' nomad job run job-instance.nomad;
# working-directory: ./homepage/homepage-jazz
envsubst '${DOCKER_USER} ${DOCKER_PASSWORD} ${DOCKER_TAG} ${BRANCH_SUFFIX} ${BRANCH_SUBDOMAIN}' < job-template.nomad > job-instance.nomad;
cat job-instance.nomad;
NOMAD_ADDR=${{ secrets.NOMAD_ADDR }} nomad job run job-instance.nomad;
working-directory: ./homepage

17
DOCS.md
View File

@@ -16010,15 +16010,19 @@ TODO: doc generator not implemented yet 2097152
# jazz-nodejs
## `createOrResumeWorker(workerName, syncServer?)`
## `createOrResumeWorker({workerName, syncServer?, migration?})`
<sup>(function in `jazz-nodejs`)</sup>
```typescript
export function createOrResumeWorker(workerName: string, syncServer: string): Promise<{
export function createOrResumeWorker<P extends Profile<ProfileShape, ProfileMeta>, R extends CoMap<{
[key: string]: JsonValue | undefined }, null | JsonObject>>({
workerName: string,
syncServer?: string,
migration?: AccountMigration<P, R>,
}): Promise<{
localNode: LocalNode,
worker: ControlledAccount<Profile<ProfileShape, ProfileMeta>, CoMap<{
[key: string]: JsonValue | undefined }, null | JsonObject>, AccountMeta>,
worker: ControlledAccount<P, R, AccountMeta>,
}>
```
TODO: document
@@ -16027,8 +16031,9 @@ TODO: document
| name | description |
| ----: | ---- |
| `workerName` | TODO: document |
| `syncServer?` | TODO: document |
| `__namedParameters.workerName` | TODO: document |
| `__namedParameters.syncServer?` | TODO: document |
| `__namedParameters.migration?` | TODO: document |

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
examples/chat-passphrase/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,17 @@
# jazz-example-chat
## 0.0.47
### Patch Changes
- Implement passphrase based auth
- Updated dependencies
- jazz-react-auth-passphrase@0.5.1
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,64 @@
# Jazz Todo List Example
Live version: https://example-todo.jazz.tools
## Installing & running the example locally
Start by checking out just the example app to a folder:
```bash
npx degit gardencmp/jazz/examples/todo jazz-example-todo
cd jazz-example-todo
```
(This ensures that you have the example app without git history or our multi-package monorepo)
Install dependencies:
```bash
npm install
```
Start the dev server:
```bash
npm run dev
```
## Structure
- [`src/basicComponents`](./src/basicComponents): simple components to build the UI, unrelated to Jazz (uses [shadcn/ui](https://ui.shadcn.com))
- [`src/components`](./src/components/): helper components that do contain Jazz-specific logic, but aren't very relevant to understand the basics of Jazz and CoJSON
- [`src/1_types.ts`](./src/1_types.ts),
[`src/2_main.tsx`](./src/2_main.tsx),
[`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx),
[`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx): the main files for this example, see the walkthrough below
## Walkthrough
### Main parts
1. Defining the data model with CoJSON: [`src/1_types.ts`](./src/1_types.ts)
2. The top-level provider `<WithJazz/>` and routing: [`src/2_main.tsx`](./src/2_main.tsx)
3. Creating a new todo project: [`src/3_NewProjectForm.tsx`](./src/3_NewProjectForm.tsx)
4. Reactively rendering a todo project as a table, adding and editing tasks: [`src/4_ProjectTodoTable.tsx`](./src/4_ProjectTodoTable.tsx)
### Helpers
- (not yet explained) Creating invite links/QR codes with `<InviteButton/>`: [`src/components/InviteButton.tsx`](./src/components/InviteButton.tsx)
This is the whole Todo List app!
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<WithJazz>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<link rel="stylesheet" href="/src/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Chat Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
job "chat$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "chat$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,48 @@
{
"name": "jazz-example-chat-passphrase",
"private": true,
"version": "0.0.47",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"hash-slash": "^0.1.3",
"jazz-react": "^0.5.0",
"jazz-react-auth-passphrase": "^0.5.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,45 @@
import { WithJazz, useJazz } from 'jazz-react';
import { PassphraseAuth, PassphraseAuthBasicUI } from 'jazz-react-auth-passphrase';
import ReactDOM from 'react-dom/client';
import { HashRoute } from 'hash-slash';
import { ChatWindow } from './chatWindow.tsx';
import { Chat } from './dataModel.ts';
import {wordlist} from '@scure/bip39/wordlists/english';
ReactDOM.createRoot(document.getElementById('root')!).render(
<WithJazz
auth={PassphraseAuth({
appName: 'Jazz Chat Example',
wordlist,
Component: PassphraseAuthBasicUI
})}
apiKey="api_z9d034j3t34ht034ir"
>
<App />
</WithJazz>,
);
function App() {
return <div className='flex flex-col items-center justify-between w-screen h-screen p-2 dark:bg-black dark:text-white'>
<button onClick={useJazz().logOut} className='rounded mb-5 px-2 py-1 bg-stone-200 dark:bg-stone-800 dark:text-white self-end'>
Log Out
</button>
{HashRoute({
'/': <Home />,
'/chat/:id': (id) => <ChatWindow chatId={id as Chat['id']} />,
}, { reportToParentFrame: true })}
</div>
}
function Home() {
const { me } = useJazz();
return <button className='rounded py-2 px-4 bg-stone-200 dark:bg-stone-800 dark:text-white my-auto'
onClick={() => {
const group = me.createGroup().addMember('everyone', 'writer');
const chat = group.createList<Chat>();
location.hash = '/chat/' + chat.id;
}}>
Create New Chat
</button>
}

View File

@@ -0,0 +1,43 @@
import { useAutoSub } from 'jazz-react';
import { Chat, Message } from './dataModel.ts';
export function ChatWindow(props: { chatId: Chat['id'] }) {
const chat = useAutoSub(props.chatId);
return chat ? <div className='w-full max-w-xl h-full flex flex-col items-stretch'>
{
chat.map((msg, i) => (
<ChatBubble key={msg?.id}
text={msg?.text}
by={chat.meta.edits[i].by?.profile?.name}
byMe={chat.meta.edits[i].by?.isMe}
at={chat.meta.edits[i].at} />
))
}
<ChatInput onSubmit={(text) => {
const msg = chat.meta.group.createMap<Message>({ text });
chat.append(msg.id);
}}/>
</div> : <div>Loading...</div>;
}
function ChatBubble(props: { text?: string, by?: string, at?: Date, byMe?: boolean }) {
return <div className={`${props.byMe ? 'items-end' : 'items-start'} flex flex-col`}>
<div className='rounded-xl bg-stone-100 dark:bg-stone-700 dark:text-white py-2 px-4 mt-2 min-w-[5rem]'>
{ props.text }
</div>
<div className='text-xs text-neutral-500 ml-2'>
{ props.by } { props.at?.getHours() }:{ props.at?.getMinutes() }
</div>
</div>;
}
function ChatInput(props: { onSubmit: (text: string) => void }) {
return <input className='rounded p-2 border mt-auto dark:bg-black dark:text-white dark:border-stone-700'
placeholder='Type a message and press Enter'
onKeyDown={({ key, currentTarget: input }) => {
if (key !== 'Enter' || !input.value) return;
props.onSubmit(input.value);
input.value = '';
}}/>
}

View File

@@ -0,0 +1,4 @@
import { CoMap, CoList } from 'cojson';
export type Chat = CoList<Message['id']>;
export type Message = CoMap<{ text: string }>;

View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
margin: 0;
padding: 0;
}
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

24
examples/counter-js-auth0/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Counter Example (Auth0)</title>
</head>
<body>
<div id="root">
<div id="count">...initializing</div>
<button id="increment">Increment</button>
<button id="login">Login</button>
</div>
<script type="module" src="/index.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
import * as auth0 from "@auth0/auth0-spa-js";
import { Account, CoID, CoList, CoMap, Profile } from "cojson";
import {
ResolvedAccount,
autoSub,
autoSubResolution,
createBrowserNode,
} from "jazz-browser";
import { BrowserAuth0 } from "jazz-browser-auth0";
const auth0options = {
domain: "dev-12uyj8w4t4yjzkwa.us.auth0.com",
clientId: "TcYtq9an3PDyInQJvD1k8PtqupYG4PnA",
};
const authorizationParams = {
redirect_uri: window.location.origin,
audience: `https://${auth0options.domain}/api/v2/`,
scope: "read:current_user update:current_user_metadata",
};
window.onload = async () => {
const auth0Client = await auth0.createAuth0Client(auth0options);
const query = window.location.search;
if (query.includes("code=") && query.includes("state=")) {
await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, "/");
} else {
document
.getElementById("login")
?.addEventListener("click", async () => {
await auth0Client.loginWithRedirect({
authorizationParams,
});
});
}
if (!(await auth0Client.isAuthenticated())) {
return;
}
let accessToken: string | undefined;
try {
accessToken = await auth0Client.getTokenSilently({
authorizationParams,
});
} catch (e) {
alert(
"Failed to get access token silently, creating popup - this should only happen on localhost. Otherwise, check that allowed callback URLs are set correctly."
);
accessToken = await auth0Client.getTokenWithPopup({
authorizationParams,
});
}
if (!accessToken) {
throw new Error("No access token");
}
const { node, done } = await createBrowserNode({
auth: new BrowserAuth0(
{
domain: auth0options.domain,
clientID: auth0options.clientId,
accessToken,
},
auth0options.domain,
"jazz-browser-auth0-example"
),
migration: (account) => {
if (!account.get("root")) {
account.set(
"root",
account.createMap({
countIncrements: account.createList([]).id,
}).id
);
}
},
});
autoSub<Account<Profile, CoMap<{ countIncrements: CoID<CoList<number>> }>>>(
"me",
node,
(me) => {
if (me?.root?.countIncrements) {
document.getElementById("count")!.innerText =
me.root.countIncrements.reduce((sum, inc) => sum + inc, 0) +
"";
}
}
);
const increments = await autoSubResolution(
"me",
(
me: ResolvedAccount<
Account<
Profile,
CoMap<{ countIncrements: CoID<CoList<number>> }>
>
>
) => {
return me?.root?.countIncrements;
},
node
);
document.getElementById("increment")!.addEventListener("click", () => {
increments.append(1);
});
};

View File

@@ -0,0 +1,56 @@
job "counter-js-auth0$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "counter-js-auth0$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,22 @@
{
"name": "counter-js-auth0",
"private": true,
"version": "0.0.46",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@auth0/auth0-spa-js": "^2.1.2",
"jazz-browser": "^0.6.0",
"jazz-browser-auth0": "^0.6.2"
},
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

View File

@@ -23,4 +23,5 @@ dist-ssr
*.sln
*.sw?
.env
.env
TwitAllTwitsCreatorCredentials.json

View File

@@ -1,5 +1,24 @@
# twit-stresstest
## 0.2.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- jazz-nodejs@0.6.0
## 0.1.1
### Patch Changes
- Allow account migrations to be async
- Updated dependencies
- jazz-nodejs@0.5.3
## 0.1.0
### Minor Changes

View File

@@ -10,12 +10,12 @@ import {
migration,
} from "../twit/src/1_dataModel.js";
import { createOrResumeWorker, autoSub } from "jazz-nodejs"
import { createOrResumeWorker, autoSub } from "jazz-nodejs";
async function runner() {
const { localNode: node, worker } = await createOrResumeWorker(
"TwitStressTestBot" + Math.random().toString(36).slice(2),
);
const { localNode: node, worker } = await createOrResumeWorker({
workerName: "TwitStressTestBot" + Math.random().toString(36).slice(2),
});
console.log(
"profile",
@@ -45,8 +45,8 @@ async function runner() {
if (startedPosting) return;
startedPosting = true;
for (let i = 0; i < 10; i++) {
await new Promise((resolve) =>
setTimeout(resolve, Math.random() * 120000)
await new Promise(
(resolve) => setTimeout(resolve, Math.random() * 120000)
// setTimeout(resolve, Math.random() * 5000)
);
const audience = me.root.peopleWhoCanSeeMyContent;
@@ -70,7 +70,7 @@ async function runner() {
let blackHole = 0;
let lastUpdate = Date.now()
let lastUpdate = Date.now();
autoSub(ALL_TWEETS_LIST_ID, node, (allTwits) => {
if (Date.now() - lastUpdate < 33) return;

View File

@@ -6,9 +6,10 @@ import {
import { createOrResumeWorker, autoSub } from "jazz-nodejs"
const { localNode: node, worker } = await createOrResumeWorker(
"TwitAllTwitsCreator"
);
const { localNode: node, worker } = await createOrResumeWorker({
workerName: "TwitAllTwitsCreator",
migration
});
const allTweetsGroup = worker.createGroup();
allTweetsGroup.addMember('everyone', 'writer');

View File

@@ -1,12 +1,12 @@
{
"name": "twit-stresstest",
"version": "0.1.0",
"version": "0.2.0",
"main": "dist/twit-stresstest/index.js",
"type":"module",
"type": "module",
"license": "MIT",
"private": true,
"dependencies": {
"jazz-nodejs": "^0.5.0"
"jazz-nodejs": "^0.6.0"
},
"scripts": {
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist",

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -49,7 +49,7 @@ export default function RootLayout({
mainLogo={<JazzLogo className="w-24" />}
items={[
{ title: "Toolkit", href: "/" },
{ title: "Global Mesh", href: "/mesh" },
{ title: "Mesh / Pricing", href: "/mesh" },
{
title: "Docs & Guides",
href: "https://github.com/gardencmp/jazz/blob/main/DOCS.md",

248
homepage/app/mesh/page.mdx Normal file
View File

@@ -0,0 +1,248 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { fmtPrice, pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Jazz Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Mesh
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
<Grid>
<GridCard>
### Mesh Free
<span className="text-2xl">$0</span>
- Unlimited projects
- For individual developers
- Low-latency sync
- Egress/mo: 5 million ops <span className="text-xs">or 50GB blobs</span>
- Storage: 2.5 million ops <span className="text-xs">or 25GB blobs</span>
</GridCard>
<GridCard>
### Mesh Starter <ComingSoonBadge/>
<span className="text-2xl">$9</span>/developer/mo
- Unlimited projects
- Up to 3 developers
- Low-latency sync
- Egress/mo: 50 million ops <span className="text-xs">or 500GB blobs</span>
- Storage: 25 million ops <span className="text-xs">or 250GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
<GridCard>
### Mesh Pro <ComingSoonBadge/>
<span className="text-2xl">$19</span>/developer/mo
- Unlimited projects
- Up to 50 developers, SSO/SAML
- Ultra-low-latency sync
- Egress/mo: 100 million ops <span className="text-xs">or 1TB blobs</span>
- Storage: 50 million ops <span className="text-xs">or 500GB blobs</span>
<div className="text-xs">
- Extra egress: {fmtPrice(10 * pricePer1MtxSyncedOut)} per 10 million ops or 100GB blobs
- Extra storage: {fmtPrice(10 * pricePer1MtxStored)} per 10 million ops or 100GB blobs
</div>
</GridCard>
{/*<GridCard>
### Mesh Enterprise <ComingSoonBadge/>
<span className="text-2xl">Custom</span>
- Custom SLA
- Custom cloud deployment
- Dedicated support
- Audit logs
</GridCard>*/}
</Grid>
An operation represents an **individual user action**, or **10KB of data** for blobs/streams.
<Grid>
<GridItem className="col-start-1">
#### Egress:
<div className="text-sm">
- Operations sent out from Jazz Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each op should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Operations stored:
<div className="text-sm">
- Operations that are continuously persisted.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of ops generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- **Session A: 4 users co-oping 10 pages of text, typing them out as individual character inserts:**
- 3,000 inserts/page &times; 10 pages = 30,000 ops -> 30k ops stored
- 30,000 ops, each synced out to 3 other users -> 90k ops egress (one-time)
- **You could have ~50 such sessions/mo within Mesh Free** and you can keep storing ~80 such texts.
- **You could have ~500 such sessions/mo within Mesh Starter included usage** and you can keep storing ~800 such texts.
- **You could have ~1000 such sessions/mo within Mesh Pro included usage** and you can keep storing ~1600 such texts.
- Each further such session would cost you about {fmtPrice(90000 * pricePerTxSyncedOut)} and {fmtPrice(30000 * pricePerTxStored)} per month to keep storing the text.
- **Session B: 3 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 5 hours**
- 3 users &times; 10 FPS &times; 10s/min &times; 60min/h &times; 5h = 90k ops -> 90k ops stored
- 90k ops, each synced out to 2 other users -> 180k ops egress (one-time)
- **You could have ~20 such sessions/mo within Mesh Free** and you can keep storing 20 such canvases.
- **You could have ~250 such sessions/mo within Mesh Starter included usage** and you can keep storing 250 such canvases.
- **You could have ~500 such sessions/mo within Mesh Pro included usage** and you can keep storing 500 such canvases.
- Each further such session would cost you about {fmtPrice(180000 * pricePerTxSyncedOut)} and {fmtPrice(90000 * pricePerTxStored)} per month to keep storing the canvas.
- **Session C: A livestreamer streaming video (1GB total) to 25 viewers (combined live & on-demand)**
- 1GB = 100,000 ops (10KB each) -> 100k ops stored
- 100,000 ops, each synced out to 25 viewers -> 2.5M ops egress (one-time)
- **You could have ~2 such livestreams/mo within Mesh Free** and you can keep storing 25 such videos.
- **You could have ~20 such livestreams/mo within Mesh Starter included usage** and you can keep storing 250 such videos.
- **You could have ~40 such livestreams/mo within Mesh Pro included usage** and you can keep storing 500 such videos.
- Each further such livestream would cost you about {fmtPrice(2500000 * pricePerTxSyncedOut)} and {fmtPrice(100000 * pricePerTxStored)} per month to keep storing the video.
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan>You can rely on Jazz Mesh. But you don't have to.</Slogan>
<p>Because Jazz is open-source, you can optionally run your own sync nodes &mdash; in a variety of setups.</p>
<Grid>
<GridCard>
#### Jazz Mesh + Data Backup Node.
<p className="no-prose text-base">Connect your users to Jazz Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Every op is additionally synced to your backup node and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Jazz Mesh + DIY Mesh.
<p className="no-prose text-base">Connect your users to Jazz Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Every op is additionally synced to your DIY mesh and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">Build your own network of sync and storage nodes.
Handle networking, security and backups yourself.</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -34,7 +34,7 @@ Jazz is an open-source toolkit for building apps with **sync** & **secure collab
<h2 className="md:mt-24">Hard things are easy now</h2>
Jazz replaces APIs, DBs and message queues with **a single new abstraction: CoJSON**.
Jazz replaces APIs, databases and message queues with **a single new abstraction: collaborative data**.
This means you get **built-in capabilities** that took best-in-class apps years to build:
@@ -71,20 +71,7 @@ This means you get **built-in capabilities** that took best-in-class apps years
</Grid>
</div>
## CoJSON
<Slogan small>The collaborative core.</Slogan>
Jazz is built around **CoJSON,** a new abstraction for **sync** & **secure collaborative data.** And while it does all the heavy lifting...
- **multi-device co-editing**
- **user identities & accounts**
- **permissions** & **roles**
- **sync** & **caching**
- **persistence**
...its API couldn't be simpler: CoJSON makes collaboration and secure access control feel like **inherent properties of your data**.
### Collaborative Values
## Collaborative Values
<Slogan small>Your new building blocks.</Slogan>
@@ -99,7 +86,7 @@ CoValues also **keep their full edit history,** including author metadata and po
<div className="text-sm">
- Collaborative key-value map
- Possible values:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
</div>
</GridCard>
<GridCard>
@@ -107,7 +94,7 @@ CoValues also **keep their full edit history,** including author metadata and po
<div className="text-sm">
- Collaborative ordered list
- Possible items:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
</div>
</GridCard>
<GridItem className="col-span-full lg:col-span-1 mb-10 lg:ml-4 [&>p]:m-0 pt-4">
@@ -139,7 +126,7 @@ A shocking amount of UI is text editing. CoJSON offers correct, versatile primit
### `CoStream`
<div className="text-sm">
- Collection of independent per-user items streams:
- Immutable JSON & IDs of other CoValues
- Immutable JSON & other CoValues
- Great for presence, reactions, polls, replies etc.
</div>
</GridCard>
@@ -185,7 +172,7 @@ A simple API to define access control from anywhere, verifiably enforced by encr
## The Jazz Toolkit
<Slogan small>Idiomatic bindings for CoJSON, with batteries included.</Slogan>
<Slogan small>A high-level toolkit for building apps around CoValues.</Slogan>
Supported environments:
<div className="text-sm">
@@ -264,15 +251,15 @@ Supported environments:
</GridCard>
</Grid>
## Global Mesh
## Jazz Mesh
<Slogan small>Serverless sync & storage for Jazz apps</Slogan>
To give you sync & secure collaborative data instantly on a global scale, we're running Global Mesh. It works with any CoJSON-based app, requires no setup and has straightforward, scale-to-zero pricing.
To give you sync and secure collaborative data instantly on a global scale, we're running Jazz Mesh. It works with any Jazz-based app, requires no setup and has straightforward, scale-to-zero pricing.
Global Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
Jazz Mesh is currently free &mdash; and it's set up as the default sync & storage peer in Jazz, letting you start building multi-user apps with persistence right away, no backend needed.
<Link href="/mesh" target="_blank">Learn more about Global Mesh</Link>
<Link href="/mesh" target="_blank">Learn more about Jazz Mesh</Link>
## Get Started

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,19 @@
import { ComingSoonBadge, Grid, GridCard, GridItem } from "./forMdx";
export const pricePer1MtxSyncedOut = 1;
export const pricePer1MtxStored = 2;
export const pricePer1MtxSyncedOut = 0.1;
export const pricePer1MtxStored = 0.2;
export const pricePerTxSyncedOut = pricePer1MtxSyncedOut / 1_000_000;
export const pricePerTxStored = pricePer1MtxStored / 1_000_000;
export function fmtPrice(raw: number) {
return raw.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumSignificantDigits: 2,
});
}
export function Pricing() {
const worstCaseBytesPerTx = 200_000;

View File

@@ -1,202 +0,0 @@
import { Slogan, Grid, GridCard, GridItem, ComingSoonBadge } from '@/components/forMdx';
import { pricePer1MtxSyncedOut, pricePerTxSyncedOut, pricePer1MtxStored, pricePerTxStored } from '@/components/pricing';
export const metadata = {
title: "jazz - Global Mesh",
description: "Serverless sync & storage for Jazz apps.",
};
# Jazz Global Mesh
<Slogan>Serverless sync & storage for Jazz apps.</Slogan>
Real-time sync and storage infrastructure that scales up to millions of users.<br/>
Pricing that scales down to zero.
## The first Collaboration Delivery Network
<Slogan small>Build demanding apps with distributed state, backed by a new kind of cloud.</Slogan>
<Grid>
<GridCard>
#### Optimal mesh routing.
Get ultra-low latency between any group of users with our decentralized mesh interconnect.
</GridCard>
<GridCard>
#### Smart caching.
Give users instant load times, with their latest data state always cached close to them.
</GridCard>
<GridCard>
#### Blob storage & media streaming.
Store files and media streams as idiomatic `CoValues` without S3.
</GridCard>
</Grid>
## Pricing
<Slogan small></Slogan>
### Free Tier
<span className="text-lg font-medium bg-emerald-200 dark:bg-emerald-800 px-2 py-1 rounded">Until we implement billing all usage of Global Mesh is free!</span>
<p className="text-sm">Later, any usage under $1/mo will be free.</p>
<Grid>
<GridItem className="md:col-span-2">
### Unlimited <ComingSoonBadge/>
<div className="lg:text-2xl border rounded-lg px-1 py-3 text-center">${pricePer1MtxSyncedOut} <small>per 1M TXs synced out</small> + ${pricePer1MtxStored}<small>/mo per 1M TXs stored</small></div>
<p><small>$6/mo minimum usage</small></p>
A TX (transaction) represents an **individual user action**, or **up to 100KB of binary data**.
</GridItem>
<GridItem className="col-start-1">
#### Transactions synced out:
<div className="text-sm">
- Transactions sent out from Global Mesh, each counted once for every device it is synced out to.
- Depending on cache behavior each transaction should only be synced out once per connection, ideally once per device requesting it.
</div>
</GridItem>
<GridItem>
#### Transactions stored:
<div className="text-sm">
- Transactions that are continuously persisted.
- Counted per second.
- Includes backups, hot storage and edge caches.
</div>
</GridItem>
</Grid>
**Examples:**
The number of transactions generated is highly app-specific and depends on user behaviour, but here are some examples:
<div className="text-sm">
- 4 users co-editing 10 pages of text, typing them out as individual character inserts:
- 3,000 inserts/page &times; 10 pages = 30,000 transactions
- 30,000 transactions stored = ${30000 * pricePerTxStored} / mo
- 3 &times; 30,000 transactions synced out = ${3 * 30000 * pricePerTxSyncedOut} one-time
- 4 users collaborating on a canvas, moving shapes around at 10 FPS for 10s/min for 2h/day for a month
- 4 users &times; 10 FPS &times; 10s/min &times; 60min/h * 2h/day &times; 30days = 1.44M transactions
- 1.44M transactions stored = ${1440000 * pricePerTxStored} / mo = ${1440000 * pricePerTxStored / 4} / mo / user
- 3 &times; 1.44M transactions synced out = ${(3 * 1440000 * pricePerTxSyncedOut).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time = ${(3 * 1440000 * pricePerTxSyncedOut / 4).toLocaleString("en-US", { maximumSignificantDigits: 3, })} one-time / user
- A livestreamer streaming video (1GB total) to 100 viewers (combined live & on-demand)
- 1GB = 10,000 transactions (100KB each)
- 10,000 transactions stored = ${10000 * pricePerTxStored} / mo (= ${10000 * 1000 * pricePerTxStored} per 1TB stored)
- 100 &times; 10,000 transaction synced out = ${100 * 10000 * pricePerTxSyncedOut} one-time (= ${10000 * 1000 * pricePerTxSyncedOut} per 1TB egress)
</div>
## Global Footprint
We're rapidly expanding our network of sync & storage nodes. This is our current best-effort coverage:
<Grid className="grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<GridItem>
<div className="text-sm">
**Under 50ms RTT**
- Frankfurt
- New York
- Newark
- North California
- North Virginia
- San Francisco
- Singapore
- Toronto
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 100ms RTT**
- Amsterdam
- Atlanta
- London
- Ohio
- Paris
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 200ms RTT**
- Bangalore
- Dallas
- Mumbai
- Oregon
**Under 300ms RTT**
- Seoul
- Tokyo
</div>
</GridItem>
<GridItem>
<div className="text-sm">
**Under 400ms RTT**
- Sao Paulo
- Sydney
**Under 500ms RTT**
- Cape Town
</div>
</GridItem>
</Grid>
### Enterprise
Custom deployment in the cloud, your private cloud, on-premises or hybrids?
SLAs and dedicated support? White-glove integration services?
Let's talk: <a href="mailto:hello@gcmp.io">hello@gcmp.io</a>
## Custom Deployment Scenarios
<Slogan>You can rely on Global Mesh. But you don't have to.</Slogan>
<p>Because Jazz is open-source, you can optionally run your own sync nodes &mdash; in a variety of setups.</p>
<Grid>
<GridCard>
#### Global Mesh + Data Backup Node.
<p className="no-prose text-base">Connect your users to Global Mesh for all its benefits, but also run and connect your own data backup node (just in case.)</p>
<div className="text-sm">
Extra costs:
- Instance costs for the backup node.
- Moderate self-hosted storage costs.
- Every transaction is additionally synced to your backup node and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Global Mesh + DIY Mesh.
<p className="no-prose text-base">Connect your users to Global Mesh, or your own nodes as a lower-performance fallback. The two networks stay in constant sync.</p>
<div className="text-sm">
Extra costs:
- N × instance cost for your sync nodes.
- Typically moderate self-hosted egress costs.
- High self-hosted storage costs.
- Every transaction is additionally synced to your DIY mesh and counted as synced out.
</div>
</GridCard>
<GridCard>
#### Completely DIY Mesh.
<p className="no-prose text-base">Build your own network of sync and storage nodes.
Handle networking, security and backups yourself.</p>
<div className="text-sm">
Costs:
- N × instance cost for your sync nodes.
- Very high self-hosted egress costs.
- High self-hosted storage costs.
</div>
</GridCard>
</Grid>

View File

@@ -22,14 +22,14 @@ await rm("./codeSamples", { recursive: true, force: true });
(
await Promise.all(
(
await readdir(path.join("../../", dir))
await readdir(path.join("../", dir))
).map(async (f) =>
(f.endsWith(".ts") && f !== "vite-env.d.ts") ||
f.endsWith(".tsx")
? [
f,
await readFile(
path.join("../../", dir, f),
path.join("../", dir, f),
"utf8"
),
]

View File

@@ -18,5 +18,6 @@
"updated": "lerna updated --include-merged-tags",
"publish-all": "yarn run gen-docs && lerna publish --include-merged-tags",
"gen-docs": "ts-node generateDocs.ts"
}
},
"version": "0.0.0"
}

View File

@@ -1,5 +1,17 @@
# cojson-simple-sync
## 0.6.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- cojson@0.6.0
- cojson-storage-sqlite@0.5.2
## 0.5.0
### Minor Changes

View File

@@ -4,7 +4,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.5.0",
"version": "0.6.0",
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/ws": "^8.5.5",
@@ -16,8 +16,8 @@
"typescript": "5.0.2"
},
"dependencies": {
"cojson": "^0.5.0",
"cojson-storage-sqlite": "^0.5.0",
"cojson": "^0.6.0",
"cojson-storage-sqlite": "^0.5.2",
"ws": "^8.13.0"
},
"scripts": {

View File

@@ -1,4 +1,4 @@
import { AnonymousControlledAccount, LocalNode, cojsonInternals, cojsonReady } from "cojson";
import { ControlledAgent, LocalNode, cojsonInternals, cojsonReady } from "cojson";
import { WebSocketServer } from "ws";
import { SQLiteStorage } from "cojson-storage-sqlite";
import { websocketReadableStream, websocketWritableStream } from "./websocketStreams.js";
@@ -15,7 +15,7 @@ const agentSecret = cojsonInternals.newRandomAgentSecret();
const agentID = cojsonInternals.getAgentID(agentSecret);
const localNode = new LocalNode(
new AnonymousControlledAccount(agentSecret),
new ControlledAgent(agentSecret),
cojsonInternals.newRandomSessionID(agentID)
);

View File

@@ -1,5 +1,30 @@
# cojson-storage-indexeddb
## 0.6.2
### Patch Changes
- Fix TypeScript lint
## 0.6.1
### Patch Changes
- IndexedDB & timer perf improvements
- Updated dependencies
- cojson@0.6.4
## 0.6.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- cojson@0.6.0
## 0.5.0
### Minor Changes

View File

@@ -1,11 +1,11 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.5.0",
"version": "0.6.2",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.5.0",
"cojson": "^0.6.4",
"typescript": "^5.1.6"
},
"devDependencies": {

View File

@@ -1,12 +1,12 @@
import { expect, test } from "vitest";
import { AnonymousControlledAccount, LocalNode, cojsonInternals } from "cojson";
import { ControlledAgent, LocalNode, cojsonInternals } from "cojson";
import { IDBStorage } from ".";
test.skip("Should be able to initialize and load from empty DB", async () => {
const agentSecret = cojsonInternals.newRandomAgentSecret();
const node = new LocalNode(
new AnonymousControlledAccount(agentSecret),
new ControlledAgent(agentSecret),
cojsonInternals.newRandomSessionID(
cojsonInternals.getAgentID(agentSecret)
)
@@ -27,7 +27,7 @@ test("Should be able to sync data to database and then load that from a new node
const agentSecret = cojsonInternals.newRandomAgentSecret();
const node1 = new LocalNode(
new AnonymousControlledAccount(agentSecret),
new ControlledAgent(agentSecret),
cojsonInternals.newRandomSessionID(
cojsonInternals.getAgentID(agentSecret)
)
@@ -50,7 +50,7 @@ test("Should be able to sync data to database and then load that from a new node
await new Promise((resolve) => setTimeout(resolve, 200));
const node2 = new LocalNode(
new AnonymousControlledAccount(agentSecret),
new ControlledAgent(agentSecret),
cojsonInternals.newRandomSessionID(
cojsonInternals.getAgentID(agentSecret)
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,228 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */
const isFunction = (func: any) => typeof func === 'function';
const isObject = (supposedObject: any) =>
typeof supposedObject === 'object' &&
supposedObject !== null &&
!Array.isArray(supposedObject);
const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);
const identity = (val: any) => val;
export { isObject, isThenable, identity, isFunction };
enum States {
PENDING = 'PENDING',
RESOLVED = 'RESOLVED',
REJECTED = 'REJECTED',
}
interface Handler<T, U> {
onSuccess: HandlerOnSuccess<T, U>;
onFail: HandlerOnFail<U>;
}
type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;
type Finally<U> = () => U | Thenable<U>;
interface Thenable<T> {
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: HandlerOnFail<U>,
): Thenable<U>;
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: (reason: any) => void,
): Thenable<U>;
}
type Resolve<R> = (value?: R | Thenable<R>) => void;
type Reject = (value?: any) => void;
export class SyncPromise<T> {
private state: States = States.PENDING;
private handlers: Handler<T, any>[] = [];
private value: T | any;
public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
try {
callback(this.resolve as Resolve<T>, this.reject);
} catch (e) {
this.reject(e);
}
}
private resolve = (value: T) => {
return this.setResult(value, States.RESOLVED);
};
private reject = (reason: any) => {
return this.setResult(reason, States.REJECTED);
};
private setResult = (value: T | any, state: States) => {
const set = () => {
if (this.state !== States.PENDING) {
return null;
}
if (isThenable(value)) {
return (value as Thenable<T>).then(this.resolve, this.reject);
}
this.value = value;
this.state = state;
return this.executeHandlers();
};
set();
};
private executeHandlers = () => {
if (this.state === States.PENDING) {
return null;
}
this.handlers.forEach((handler) => {
if (this.state === States.REJECTED) {
return handler.onFail(this.value);
}
return handler.onSuccess(this.value);
});
this.handlers = [];
};
private attachHandler = (handler: Handler<T, any>) => {
this.handlers = [...this.handlers, handler];
this.executeHandlers();
};
public then<U>(
onSuccess: HandlerOnSuccess<T, U>,
onFail?: HandlerOnFail<U>,
) {
return new SyncPromise<U>((resolve, reject) => {
return this.attachHandler({
onSuccess: (result) => {
try {
return resolve(onSuccess(result));
} catch (e) {
return reject(e);
}
},
onFail: (reason) => {
if (!onFail) {
return reject(reason);
}
try {
return resolve(onFail(reason));
} catch (e) {
return reject(e);
}
},
});
});
}
public catch<U>(onFail: HandlerOnFail<U>) {
return this.then<U>(identity, onFail);
}
// methods
public toString() {
return `[object SyncPromise]`;
}
public finally<U>(cb: Finally<U>) {
return new SyncPromise<U>((resolve, reject) => {
let val: U | any;
let isRejected: boolean;
return this.then(
(value) => {
isRejected = false;
val = value;
return cb();
},
(reason) => {
isRejected = true;
val = reason;
return cb();
},
).then(() => {
if (isRejected) {
return reject(val);
}
return resolve(val);
});
});
}
public spread<U>(handler: (...args: any[]) => U) {
return this.then<U>((collection) => {
if (Array.isArray(collection)) {
return handler(...collection);
}
return handler(collection);
});
}
// static
public static resolve<U = any>(value?: U | Thenable<U>) {
return new SyncPromise<U>((resolve) => {
return resolve(value);
});
}
public static reject<U>(reason?: any) {
return new SyncPromise<U>((_resolve, reject) => {
return reject(reason);
});
}
public static all<U = any>(collection: (U | Thenable<U>)[]) {
return new SyncPromise<U[]>((resolve, reject) => {
if (!Array.isArray(collection)) {
return reject(new TypeError('An array must be provided.'));
}
if (collection.length === 0) {
return resolve([]);
}
let counter = collection.length;
const resolvedCollection: U[] = [];
const tryResolve = (value: U, index: number) => {
counter -= 1;
resolvedCollection[index] = value;
if (counter !== 0) {
return null;
}
return resolve(resolvedCollection);
};
return collection.forEach((item, index) => {
return SyncPromise.resolve(item)
.then((value) => {
return tryResolve(value, index);
})
.catch(reject);
});
});
}
}

View File

@@ -1,5 +1,12 @@
# cojson-storage-sqlite
## 0.5.2
### Patch Changes
- Updated dependencies
- cojson@0.6.0
## 0.5.1
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.5.1",
"version": "0.5.2",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "^0.5.0",
"cojson": "^0.6.0",
"typescript": "^5.1.6",
"@types/better-sqlite3": "^7.6.4"
},

View File

@@ -1,5 +1,12 @@
# cojson-transport-nodejs-ws
## 0.5.1
### Patch Changes
- Updated dependencies
- cojson@0.6.0
## 0.5.0
### Minor Changes

View File

@@ -1,12 +1,12 @@
{
"name": "cojson-transport-nodejs-ws",
"type": "module",
"version": "0.5.0",
"version": "0.5.1",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "^0.5.0",
"cojson": "^0.6.0",
"typescript": "^5.1.6",
"ws": "^8.14.2"
},

View File

@@ -1,5 +1,47 @@
# cojson
## 0.6.5
### Patch Changes
- Fix loading of accounts
## 0.6.4
### Patch Changes
- IndexedDB & timer perf improvements
## 0.6.3
### Patch Changes
- Implement passphrase based auth
## 0.6.2
### Patch Changes
- Add peersToLoadFrom for node creation as well
## 0.6.1
### Patch Changes
- Provide localNode to AccountMigrations
## 0.6.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
## 0.5.2
### Patch Changes
- Allow account migrations to be async
## 0.5.1
### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.5.1",
"version": "0.6.5",
"devDependencies": {
"@noble/curves": "^1.2.0",
"@types/jest": "^29.5.3",

View File

@@ -27,7 +27,7 @@ import { Group } from "./coValues/group.js";
import { LocalNode } from "./localNode.js";
import { CoValueKnownState, NewContentMessage } from "./sync.js";
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
import { AccountID, GeneralizedControlledAccount } from "./coValues/account.js";
import { AccountID, ControlledAccountOrAgent } from "./coValues/account.js";
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
import { coreToCoValue } from "./coreToCoValue.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
@@ -133,7 +133,7 @@ export class CoValueCore {
}
testWithDifferentAccount(
account: GeneralizedControlledAccount,
account: ControlledAccountOrAgent,
currentSessionID: SessionID
): CoValueCore {
const newNode = this.node.testWithDifferentAccount(

View File

@@ -15,6 +15,7 @@ import {
import { AgentID } from "../ids.js";
import { CoMap } from "./coMap.js";
import { Group, InviteSecret } from "./group.js";
import { LocalNode } from "../index.js";
export function accountHeaderForInitialAgentSecret(
agentSecret: AgentSecret
@@ -36,7 +37,7 @@ export class Account<
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
> extends Group<P, R, Meta> {
getCurrentAgentID(): AgentID {
currentAgentID(): AgentID {
const agents = this.keys().filter((k): k is AgentID =>
k.startsWith("sealer_")
);
@@ -51,7 +52,7 @@ export class Account<
}
}
export interface GeneralizedControlledAccount {
export interface ControlledAccountOrAgent {
id: AccountID | AgentID;
agentSecret: AgentSecret;
@@ -64,12 +65,12 @@ export interface GeneralizedControlledAccount {
/** @hidden */
export class ControlledAccount<
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
>
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
>
extends Account<P, R, Meta>
implements GeneralizedControlledAccount
implements ControlledAccountOrAgent
{
agentSecret: AgentSecret;
@@ -116,8 +117,8 @@ Meta extends AccountMeta = AccountMeta
}
/** @hidden */
export class AnonymousControlledAccount
implements GeneralizedControlledAccount
export class ControlledAgent
implements ControlledAccountOrAgent
{
agentSecret: AgentSecret;
@@ -158,10 +159,17 @@ export type ProfileShape = {
};
export type ProfileMeta = { type: "profile" };
export class Profile<Shape extends ProfileShape = ProfileShape, Meta extends ProfileMeta = ProfileMeta> extends CoMap<Shape, Meta> {
export class Profile<
Shape extends ProfileShape = ProfileShape,
Meta extends ProfileMeta = ProfileMeta
> extends CoMap<Shape, Meta> {}
}
export type AccountMigration< P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta> = (account: ControlledAccount<P, R, Meta>, profile: P) => void;
export type AccountMigration<
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
> = (
account: ControlledAccount<P, R, Meta>,
profile: P,
localNode: LocalNode
) => void | Promise<void>;

View File

@@ -18,7 +18,7 @@ import {
getAgentID,
} from "../crypto.js";
import { AgentID, isAgentID } from "../ids.js";
import { AccountID, Profile } from "./account.js";
import { Account, AccountID, ControlledAccountOrAgent, Profile } from "./account.js";
import { Role } from "../permissions.js";
import { base58 } from "@scure/base";
@@ -94,13 +94,13 @@ export class Group<
*
* @category 2. Role changing
*/
addMember(accountID: AccountID | Everyone, role: Role): this {
return this.addMemberInternal(accountID, role);
addMember(account: Account | ControlledAccountOrAgent | Everyone, role: Role): this {
return this.addMemberInternal(account, role);
}
/** @internal */
addMemberInternal(
accountID: AccountID | AgentID | Everyone,
account: Account | ControlledAccountOrAgent | AgentID | Everyone,
role: Role
): this {
return this.mutate((mutable) => {
@@ -110,15 +110,15 @@ export class Group<
throw new Error("Can't add member without read key secret");
}
if (accountID === EVERYONE) {
if (account === EVERYONE) {
if (!(role === "reader" || role === "writer")) {
throw new Error(
"Can't make everyone something other than reader or writer"
);
}
mutable.set(accountID, role, "trusting");
mutable.set(account, role, "trusting");
if (mutable.get(accountID) !== role) {
if (mutable.get(account) !== role) {
throw new Error("Failed to set role");
}
@@ -128,18 +128,16 @@ export class Group<
"trusting"
);
} else {
const agent = this.core.node.resolveAccountAgent(
accountID,
"Expected to know agent to add them to group"
);
mutable.set(accountID, role, "trusting");
const memberKey = typeof account === "string" ? account : account.id;
const agent = typeof account === "string" ? account : account.currentAgentID();
mutable.set(memberKey, role, "trusting");
if (mutable.get(accountID) !== role) {
if (mutable.get(memberKey) !== role) {
throw new Error("Failed to set role");
}
mutable.set(
`${currentReadKey.id}_for_${accountID}`,
`${currentReadKey.id}_for_${memberKey}`,
seal({
message: currentReadKey.secret,
from: this.core.node.account.currentSealerSecret(),
@@ -225,15 +223,14 @@ export class Group<
*
* @category 2. Role changing
*/
removeMember(accountID: AccountID): this {
return this.removeMemberInternal(accountID);
removeMember(account: Account | ControlledAccountOrAgent | Everyone): this {
return this.removeMemberInternal(account);
}
/** @internal */
removeMemberInternal(accountID: AccountID | AgentID): this {
const afterRevoke = this.mutate((map) => {
map.set(accountID, "revoked", "trusting");
});
removeMemberInternal(account: Account | ControlledAccountOrAgent | AgentID | Everyone): this {
const memberKey = typeof account === "string" ? account : account.id;
const afterRevoke = this.set(memberKey, "revoked", "trusting");
return afterRevoke.rotateReadKey();
}

View File

@@ -2,6 +2,7 @@ import {
CoValueCore,
newRandomSessionID,
MAX_RECOMMENDED_TX_SIZE,
idforHeader,
} from "./coValueCore.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { LocalNode } from "./localNode.js";
@@ -27,7 +28,7 @@ import {
} from "./crypto.js";
import { connectedPeers } from "./streamUtils.js";
import {
AnonymousControlledAccount,
ControlledAgent,
ControlledAccount,
} from "./coValues/account.js";
import type { Role } from "./permissions.js";
@@ -36,7 +37,7 @@ import { Group, EVERYONE } from "./coValues/group.js";
import type { Everyone } from "./coValues/group.js";
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
import { parseJSON } from "./jsonStringify.js";
import { Account, Profile } from "./coValues/account.js";
import { Account, Profile, accountHeaderForInitialAgentSecret } from "./coValues/account.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
@@ -80,6 +81,8 @@ export const cojsonInternals = {
parseJSON,
accountOrAgentIDfromSessionID,
isAccountID,
accountHeaderForInitialAgentSecret,
idforHeader
};
export {
@@ -108,7 +111,7 @@ export {
SessionID,
Media,
CoValueCore,
AnonymousControlledAccount,
ControlledAgent,
ControlledAccount,
cryptoReady as cojsonReady,
MAX_RECOMMENDED_TX_SIZE,

View File

@@ -26,9 +26,9 @@ import {
Account,
AccountMeta,
accountHeaderForInitialAgentSecret,
GeneralizedControlledAccount,
ControlledAccountOrAgent,
ControlledAccount,
AnonymousControlledAccount,
ControlledAgent,
AccountID,
Profile,
AccountMigration,
@@ -52,7 +52,7 @@ export class LocalNode {
/** @internal */
coValues: { [key: RawCoID]: CoValueState } = {};
/** @category 3. Low-level */
account: GeneralizedControlledAccount;
account: ControlledAccountOrAgent;
/** @category 3. Low-level */
currentSessionID: SessionID;
/** @category 3. Low-level */
@@ -60,7 +60,7 @@ export class LocalNode {
/** @category 3. Low-level */
constructor(
account: GeneralizedControlledAccount,
account: ControlledAccountOrAgent,
currentSessionID: SessionID
) {
this.account = account;
@@ -68,27 +68,29 @@ export class LocalNode {
}
/** @category 2. Node Creation */
static withNewlyCreatedAccount<
static async withNewlyCreatedAccount<
P extends Profile = Profile,
R extends CoMap = CoMap,
Meta extends AccountMeta = AccountMeta
>({
name,
peersToLoadFrom,
migration,
initialAgentSecret = newRandomAgentSecret(),
}: {
name: string;
peersToLoadFrom?: Peer[];
migration?: AccountMigration<P, R, Meta>;
initialAgentSecret?: AgentSecret;
}): {
}): Promise<{
node: LocalNode;
accountID: AccountID;
accountSecret: AgentSecret;
sessionID: SessionID;
} {
}> {
const throwawayAgent = newRandomAgentSecret();
const setupNode = new LocalNode(
new AnonymousControlledAccount(throwawayAgent),
new ControlledAgent(throwawayAgent),
newRandomSessionID(getAgentID(throwawayAgent))
);
@@ -107,8 +109,14 @@ export class LocalNode {
"After creating account"
);
if (peersToLoadFrom) {
for (const peer of peersToLoadFrom) {
nodeWithAccount.syncManager.addPeer(peer);
}
}
if (migration) {
migration(accountOnNodeWithAccount, profile as P);
await migration(accountOnNodeWithAccount, profile as P, nodeWithAccount);
}
nodeWithAccount.account = new ControlledAccount(
@@ -156,16 +164,16 @@ export class LocalNode {
migration?: AccountMigration<P, R, Meta>;
}): Promise<LocalNode> {
const loadingNode = new LocalNode(
new AnonymousControlledAccount(accountSecret),
new ControlledAgent(accountSecret),
newRandomSessionID(accountID)
);
const accountPromise = loadingNode.load(accountID);
for (const peer of peersToLoadFrom) {
loadingNode.syncManager.addPeer(peer);
}
const accountPromise = loadingNode.load(accountID);
const account = await accountPromise;
if (account === "unavailable") {
@@ -186,6 +194,10 @@ export class LocalNode {
node.syncManager.local = node;
controlledAccount.core.node = node;
node.coValues[accountID] = {
state: "loaded",
coValue: controlledAccount.core,
};
const profileID = account.get("profile");
if (!profileID) {
@@ -194,9 +206,10 @@ export class LocalNode {
const profile = await node.load(profileID);
if (migration) {
migration(
await migration(
controlledAccount as ControlledAccount<P, R, Meta>,
profile as P
profile as P,
node
);
node.account = new ControlledAccount(
controlledAccount.core,
@@ -370,14 +383,14 @@ export class LocalNode {
const groupAsInvite = expectGroup(
group.core
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteAgentSecret),
new ControlledAgent(inviteAgentSecret),
newRandomSessionID(inviteAgentID)
)
.getCurrentContent()
);
groupAsInvite.addMemberInternal(
this.account.id,
this.account,
inviteRole === "adminInvite"
? "admin"
: inviteRole === "writerInvite"
@@ -439,7 +452,7 @@ export class LocalNode {
let account = expectGroup(
this.createCoValue(accountHeaderForInitialAgentSecret(agentSecret))
.testWithDifferentAccount(
new AnonymousControlledAccount(agentSecret),
new ControlledAgent(agentSecret),
newRandomSessionID(accountAgentID)
)
.getCurrentContent()
@@ -518,7 +531,7 @@ export class LocalNode {
);
}
return new Account(coValue).getCurrentAgentID();
return new Account(coValue).currentAgentID();
}
async resolveAccountAgentAsync(
@@ -553,7 +566,7 @@ export class LocalNode {
);
}
return new Account(coValue).getCurrentAgentID();
return new Account(coValue).currentAgentID();
}
/**
@@ -596,7 +609,7 @@ export class LocalNode {
/** @internal */
testWithDifferentAccount(
account: GeneralizedControlledAccount,
account: ControlledAccountOrAgent,
currentSessionID: SessionID
): LocalNode {
const newNode = new LocalNode(account, currentSessionID);

View File

@@ -249,7 +249,7 @@ export function determineValidTransactions(
const effectiveTransactor =
transactor === groupContent.id &&
groupAtTime instanceof Account
? groupAtTime.getCurrentAgentID()
? groupAtTime.currentAgentID()
: transactor;
const transactorRoleAtTxTime =
groupAtTime.get(effectiveTransactor) ||

View File

@@ -160,10 +160,8 @@ export function newStreamPair<T>(
// make sure write resolves before corresponding read, but make sure writes are still in order
await lastWritePromise;
lastWritePromise = new Promise((resolve) => {
setTimeout(() => {
enqueue(chunk);
resolve();
});
enqueue(chunk);
resolve();
});
}
},

View File

@@ -195,7 +195,7 @@ export class SyncManager {
return await this.handleKnownState(msg, peer);
}
case "content":
await new Promise<void>((resolve) => setTimeout(resolve, 0));
// await new Promise<void>((resolve) => setTimeout(resolve, 0));
return await this.handleNewContent(msg, peer);
case "done":
return await this.handleUnsubscribe(msg);
@@ -355,9 +355,9 @@ export class SyncManager {
e
);
});
await new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
// await new Promise<void>((resolve) => {
// setTimeout(resolve, 0);
// });
} catch (e) {
console.error(
new Date(),

View File

@@ -9,7 +9,7 @@ beforeEach(async () => {
test("Can create a node while creating a new account with profile", async () => {
const { node, accountID, accountSecret, sessionID } =
LocalNode.withNewlyCreatedAccount({ name: "Hermes Puggington" });
await LocalNode.withNewlyCreatedAccount({ name: "Hermes Puggington" });
expect(node).not.toBeNull();
expect(accountID).not.toBeNull();
@@ -22,7 +22,7 @@ test("Can create a node while creating a new account with profile", async () =>
});
test("A node with an account can create groups and and objects within them", async () => {
const { node, accountID } = LocalNode.withNewlyCreatedAccount({
const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
name: "Hermes Puggington",
});
@@ -42,7 +42,7 @@ test("A node with an account can create groups and and objects within them", asy
test("Can create account with one node, and then load it on another", async () => {
const { node, accountID, accountSecret } =
LocalNode.withNewlyCreatedAccount({ name: "Hermes Puggington" });
await LocalNode.withNewlyCreatedAccount({ name: "Hermes Puggington" });
const group = await node.createGroup();
expect(group).not.toBeNull();
@@ -69,6 +69,7 @@ test("Can create account with one node, and then load it on another", async () =
});
const map2 = await node2.load(map.id);
if (map2 === "unavailable") throw new Error("Map unavailable");
expect(map2.get("foo")).toEqual("bar");
});

View File

@@ -17,7 +17,7 @@ import {
groupWithTwoAdmins,
groupWithTwoAdminsHighLevel,
} from "./testUtils.js";
import { AnonymousControlledAccount, cojsonReady } from "../index.js";
import { ControlledAgent, cojsonReady } from "../index.js";
import { expectGroup } from "../typeUtils/expectGroup.js";
beforeEach(async () => {
@@ -70,7 +70,7 @@ test("Added adming can add a third admin to a group (high level)", () => {
const thirdAdmin = groupAsOtherAdmin.core.node.createAccount("thirdAdmin");
groupAsOtherAdmin = groupAsOtherAdmin.addMember(thirdAdmin.id, "admin");
groupAsOtherAdmin = groupAsOtherAdmin.addMember(thirdAdmin, "admin");
expect(groupAsOtherAdmin.get(thirdAdmin.id)).toEqual("admin");
});
@@ -166,7 +166,7 @@ test("Admins an add writers to a group, who can't add admins, writers, or reader
const writer = node.createAccount("writer");
group = group.addMember(writer.id, "writer");
group = group.addMember(writer, "writer");
expect(group.get(writer.id)).toEqual("writer");
const groupAsWriter = expectGroup(
@@ -179,13 +179,13 @@ test("Admins an add writers to a group, who can't add admins, writers, or reader
const otherAgent = groupAsWriter.core.node.createAccount("otherAgent");
expect(() => groupAsWriter.addMember(otherAgent.id, "admin")).toThrow(
expect(() => groupAsWriter.addMember(otherAgent, "admin")).toThrow(
"Failed to set role"
);
expect(() => groupAsWriter.addMember(otherAgent.id, "writer")).toThrow(
expect(() => groupAsWriter.addMember(otherAgent, "writer")).toThrow(
"Failed to set role"
);
expect(() => groupAsWriter.addMember(otherAgent.id, "reader")).toThrow(
expect(() => groupAsWriter.addMember(otherAgent, "reader")).toThrow(
"Failed to set role"
);
@@ -235,7 +235,7 @@ test("Admins can add readers to a group, who can't add admins, writers, or reade
const reader = node.createAccount("reader");
group = group.addMember(reader.id, "reader");
group = group.addMember(reader, "reader");
expect(group.get(reader.id)).toEqual("reader");
const groupAsReader = expectGroup(
@@ -248,13 +248,13 @@ test("Admins can add readers to a group, who can't add admins, writers, or reade
const otherAgent = groupAsReader.core.node.createAccount("otherAgent");
expect(() => groupAsReader.addMember(otherAgent.id, "admin")).toThrow(
expect(() => groupAsReader.addMember(otherAgent, "admin")).toThrow(
"Failed to set role"
);
expect(() => groupAsReader.addMember(otherAgent.id, "writer")).toThrow(
expect(() => groupAsReader.addMember(otherAgent, "writer")).toThrow(
"Failed to set role"
);
expect(() => groupAsReader.addMember(otherAgent.id, "reader")).toThrow(
expect(() => groupAsReader.addMember(otherAgent, "reader")).toThrow(
"Failed to set role"
);
@@ -337,7 +337,7 @@ test("Writers can write to an object that is owned by their group (high level)",
const writer = node.createAccount("writer");
group.addMember(writer.id, "writer");
group.addMember(writer, "writer");
const childObject = group.createMap();
@@ -396,7 +396,7 @@ test("Readers can not write to an object that is owned by their group (high leve
const reader = node.createAccount("reader");
group.addMember(reader.id, "reader");
group.addMember(reader, "reader");
const childObject = group.createMap();
@@ -548,7 +548,7 @@ test("Admins can set group read key and then writers can use it to create and re
const writer = node.createAccount("writer");
group.addMember(writer.id, "writer");
group.addMember(writer, "writer");
const childObject = group.createMap();
@@ -637,7 +637,7 @@ test("Admins can set group read key and then use it to create private transactio
const reader = node.createAccount("reader");
group.addMember(reader.id, "reader");
group.addMember(reader, "reader");
let childObject = group.createMap();
@@ -757,7 +757,7 @@ test("Admins can set group read key and then use it to create private transactio
const reader2 = node.createAccount("reader2");
group.addMember(reader1.id, "reader");
group.addMember(reader1, "reader");
let childObject = group.createMap();
@@ -774,7 +774,7 @@ test("Admins can set group read key and then use it to create private transactio
expect(childContentAsReader1.get("foo")).toEqual("bar");
group.addMember(reader2.id, "reader");
group.addMember(reader2, "reader");
const childContentAsReader2 = expectMap(
childObject.core
@@ -1013,7 +1013,7 @@ test("Admins can set group read key, make a private transaction in an owned obje
const reader = node.createAccount("reader");
group.addMember(reader.id, "reader");
group.addMember(reader, "reader");
childObject = childObject.edit((editable) => {
editable.set("foo2", "bar2", "private");
@@ -1210,8 +1210,8 @@ test("Admins can set group read rey, make a private transaction in an owned obje
const reader2 = node.createAccount("reader2");
group.addMember(reader.id, "reader");
group.addMember(reader2.id, "reader");
group.addMember(reader, "reader");
group.addMember(reader2, "reader");
childObject = childObject.edit((editable) => {
editable.set("foo2", "bar2", "private");
@@ -1314,7 +1314,7 @@ test("Admins can create an adminInvite, which can add an admin", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1363,7 +1363,7 @@ test("Admins can create an adminInvite, which can add an admin (high-level)", as
const invitedAdminID = getAgentID(invitedAdminSecret);
const nodeAsInvitedAdmin = node.testWithDifferentAccount(
new AnonymousControlledAccount(invitedAdminSecret),
new ControlledAgent(invitedAdminSecret),
newRandomSessionID(invitedAdminID)
);
@@ -1433,7 +1433,7 @@ test("Admins can create a writerInvite, which can add a writer", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1482,7 +1482,7 @@ test("Admins can create a writerInvite, which can add a writer (high-level)", as
const invitedWriterID = getAgentID(invitedWriterSecret);
const nodeAsInvitedWriter = node.testWithDifferentAccount(
new AnonymousControlledAccount(invitedWriterSecret),
new ControlledAgent(invitedWriterSecret),
newRandomSessionID(invitedWriterID)
);
@@ -1542,7 +1542,7 @@ test("Admins can create a readerInvite, which can add a reader", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1591,7 +1591,7 @@ test("Admins can create a readerInvite, which can add a reader (high-level)", as
const invitedReaderID = getAgentID(invitedReaderSecret);
const nodeAsInvitedReader = node.testWithDifferentAccount(
new AnonymousControlledAccount(invitedReaderSecret),
new ControlledAgent(invitedReaderSecret),
newRandomSessionID(invitedReaderID)
);
@@ -1651,7 +1651,7 @@ test("WriterInvites can not invite admins", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1711,7 +1711,7 @@ test("ReaderInvites can not invite admins", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1771,7 +1771,7 @@ test("ReaderInvites can not invite writers", () => {
const groupAsInvite = expectGroup(
groupCore
.testWithDifferentAccount(
new AnonymousControlledAccount(inviteSecret),
new ControlledAgent(inviteSecret),
newRandomSessionID(inviteID)
)
.getCurrentContent()
@@ -1812,7 +1812,7 @@ test("Can give read permission to 'everyone'", () => {
expect(editable.get("foo")).toEqual("bar");
});
const newAccount = new AnonymousControlledAccount(newRandomAgentSecret());
const newAccount = new ControlledAgent(newRandomAgentSecret());
const childContent2 = expectMap(
childObject
@@ -1840,12 +1840,12 @@ test("Can give read permissions to 'everyone' (high-level)", async () => {
expect(editable.get("foo")).toEqual("bar");
});
const newAccount = new AnonymousControlledAccount(newRandomAgentSecret());
const newAccount = new ControlledAgent(newRandomAgentSecret());
const childContent2 = expectMap(
childObject.core
.testWithDifferentAccount(
new AnonymousControlledAccount(newRandomAgentSecret()),
new ControlledAgent(newRandomAgentSecret()),
newRandomSessionID(newAccount.currentAgentID())
)
.getCurrentContent()
@@ -1880,7 +1880,7 @@ test("Can give write permission to 'everyone'", () => {
expect(editable.get("foo")).toEqual("bar");
});
const newAccount = new AnonymousControlledAccount(newRandomAgentSecret());
const newAccount = new ControlledAgent(newRandomAgentSecret());
const childContent2 = expectMap(
childObject
@@ -1913,7 +1913,7 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
expect(editable.get("foo")).toEqual("bar");
});
const newAccount = new AnonymousControlledAccount(newRandomAgentSecret());
const newAccount = new ControlledAgent(newRandomAgentSecret());
const childContent2 = expectMap(
childObject.core

View File

@@ -2,17 +2,17 @@ import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from
import { newRandomSessionID } from "../coValueCore.js";
import { LocalNode } from "../localNode.js";
import { expectGroup } from "../typeUtils/expectGroup.js";
import { AnonymousControlledAccount } from "../coValues/account.js";
import { ControlledAgent } from "../coValues/account.js";
import { SessionID } from "../ids.js";
// @ts-ignore
import { expect } from "bun:test";
export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccount, SessionID] {
export function randomAnonymousAccountAndSessionID(): [ControlledAgent, SessionID] {
const agentSecret = newRandomAgentSecret();
const sessionID = newRandomSessionID(getAgentID(agentSecret));
return [new AnonymousControlledAccount(agentSecret), sessionID];
return [new ControlledAgent(agentSecret), sessionID];
}
export function newGroup() {
@@ -73,7 +73,7 @@ export function groupWithTwoAdminsHighLevel() {
const otherAdmin = node.createAccount("otherAdmin");
group = group.addMember(otherAdmin.id, "admin");
group = group.addMember(otherAdmin, "admin");
return { admin, node, group, otherAdmin };
}

View File

@@ -1,5 +1,24 @@
# jazz-autosub
## 0.6.1
### Patch Changes
- Fix loading of accounts
- Updated dependencies
- cojson@0.6.5
## 0.6.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- cojson@0.6.0
## 0.5.0
### Minor Changes

View File

@@ -5,9 +5,9 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.5.0",
"version": "0.6.1",
"dependencies": {
"cojson": "^0.5.0"
"cojson": "^0.6.5"
},
"scripts": {
"test": "jest",

View File

@@ -288,9 +288,16 @@ export function autoSub(
if (!effectiveId) return () => {};
// const ctxId = Math.random().toString(16).slice(2);
// let updateN = 0;
const context = new AutoSubContext(node, () => {
const rootResolved = context.values[effectiveId]?.lastLoaded;
// const n = updateN;
// updateN++;
// console.time("AutoSubContext.onUpdate " + n + " " + ctxId);
callback(rootResolved);
// console.timeEnd("AutoSubContext.onUpdate " + n + " " + ctxId);
});
context.autoSub(effectiveId, [], "");

View File

@@ -1,5 +1,5 @@
import {
AccountID,
Account,
BinaryCoStream,
CoID,
CoList,
@@ -11,6 +11,7 @@ import {
Role,
} from "cojson";
import { AutoSubContext, ValueOrResolvedRef } from "../autoSub.js";
import { ControlledAccountOrAgent } from "cojson/src/coValues/account.js";
export class ResolvedGroupMeta<G extends Group> {
coValue!: G;
@@ -55,12 +56,12 @@ export class ResolvedGroup<G extends Group = Group> {
);
}
addMember(accountID: AccountID | Everyone, role: Role): G {
addMember(accountID: Account | ControlledAccountOrAgent | Everyone, role: Role): G {
return this.meta.group.addMember(accountID, role);
}
removeMember(accountID: AccountID): G {
return this.meta.group.removeMember(accountID);
removeMember(account: Account | ControlledAccountOrAgent | Everyone): G {
return this.meta.group.removeMember(account);
}
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {

View File

@@ -1,5 +1,24 @@
# jazz-browser-auth-local
## 0.5.0
### Minor Changes
- Make addMember and removeMember take loaded Accounts instead of just IDs
### Patch Changes
- Updated dependencies
- jazz-browser@0.6.0
## 0.4.17
### Patch Changes
- Allow account migrations to be async
- Updated dependencies
- jazz-browser@0.5.1
## 0.4.16
### Patch Changes

View File

@@ -1,11 +1,11 @@
{
"name": "jazz-browser-auth-local",
"version": "0.4.16",
"version": "0.5.0",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"jazz-browser": "^0.5.0",
"jazz-browser": "^0.6.0",
"typescript": "^5.1.6"
},
"scripts": {

View File

@@ -9,12 +9,12 @@ import {
import { agentSecretFromSecretSeed } from "cojson/src/crypto";
import { AuthProvider, SessionProvider } from "jazz-browser";
type SessionStorageData = {
type LocalStorageData = {
accountID: AccountID;
accountSecret: AgentSecret;
};
const sessionStorageKey = "jazz-logged-in-secret";
const localStorageKey = "jazz-logged-in-secret";
export interface BrowserLocalAuthDriver {
onReady: (next: {
@@ -45,16 +45,16 @@ export class BrowserLocalAuth implements AuthProvider {
initialPeers: Peer[],
migration?: AccountMigration
): Promise<LocalNode> {
if (sessionStorage[sessionStorageKey]) {
const sessionStorageData = JSON.parse(
sessionStorage[sessionStorageKey]
) as SessionStorageData;
if (localStorage[localStorageKey]) {
const localStorageData = JSON.parse(
localStorage[localStorageKey]
) as LocalStorageData;
const sessionID = await getSessionFor(sessionStorageData.accountID);
const sessionID = await getSessionFor(localStorageData.accountID);
const node = await LocalNode.withLoadedAccount({
accountID: sessionStorageData.accountID,
accountSecret: sessionStorageData.accountSecret,
accountID: localStorageData.accountID,
accountSecret: localStorageData.accountSecret,
sessionID,
peersToLoadFrom: initialPeers,
migration,
@@ -110,7 +110,7 @@ async function signUp(
const secretSeed = cojsonInternals.newRandomSecretSeed();
const { node, accountID, accountSecret } =
LocalNode.withNewlyCreatedAccount({
await LocalNode.withNewlyCreatedAccount({
name: username,
initialAgentSecret: agentSecretFromSecretSeed(secretSeed),
migration,
@@ -149,10 +149,10 @@ async function signUp(
console.log(webAuthNCredential, accountID);
sessionStorage[sessionStorageKey] = JSON.stringify({
localStorage[localStorageKey] = JSON.stringify({
accountID,
accountSecret,
} satisfies SessionStorageData);
} satisfies LocalStorageData);
node.currentSessionID = await getSessionFor(accountID);
@@ -200,10 +200,10 @@ async function logIn(
throw new Error("Invalid credential");
}
sessionStorage[sessionStorageKey] = JSON.stringify({
localStorage[localStorageKey] = JSON.stringify({
accountID,
accountSecret,
} satisfies SessionStorageData);
} satisfies LocalStorageData);
const node = await LocalNode.withLoadedAccount({
accountID,
@@ -217,5 +217,5 @@ async function logIn(
}
function logOut() {
delete sessionStorage[sessionStorageKey];
delete localStorage[localStorageKey];
}

View File

@@ -0,0 +1,21 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:require-extensions/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "require-extensions"],
parserOptions: {
project: "./tsconfig.json",
},
root: true,
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-floating-promises": "error",
},
};

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