Compare commits

...

73 Commits

Author SHA1 Message Date
Trisha Lim
d14509e22f Set specific package versions 2024-12-06 15:31:25 +00:00
Trisha Lim
f0c8340078 Remove vercel.json 2024-12-06 14:58:11 +00:00
Trisha Lim
a62aa97774 Revert to last working package.json 2024-12-06 14:54:36 +00:00
Trisha Lim
801a886efb Add vercel.json to homepage root 2024-12-06 14:47:08 +00:00
Trisha Lim
3354c0a0d8 Add vercel.json to root 2024-12-06 14:40:09 +00:00
Trisha Lim
76753454c2 Add vercel.json 2024-12-06 14:34:27 +00:00
Trisha Lim
9809666427 Update typescript 2024-12-06 14:22:05 +00:00
Trisha Lim
5fab04a8f4 Update @types/react 2024-12-06 14:13:57 +00:00
Trisha Lim
a3d3326f4b Move requestProject call to layout 2024-12-06 13:34:42 +00:00
Trisha Lim
3b22dada26 Refactor package docs 2024-12-06 11:35:35 +00:00
Benjamin S. Leveritt
9dd9366734 Merge pull request #949 from garden-co/jazz-518-write-initial-defining-schemas-docs
Write initial defining schemas docs
2024-12-05 20:23:03 +00:00
Anselm Eickhoff
48dd00f453 Merge pull request #946 from garden-co/jazz-563-fix-github-url
Replaces github url with new one
2024-12-05 18:25:57 +00:00
Anselm Eickhoff
cc361aefe5 Merge pull request #919 from garden-co/jazz-544-refactor-sqlite-storage-the-same-way-as-the-idbs-done
Refactor SQLite storage
2024-12-05 17:12:54 +00:00
Anselm
9e4438cb54 Write initial defining schemas docs 2024-12-05 17:09:01 +00:00
Benjamin S. Leveritt
08706d557a Update general web urls 2024-12-05 16:55:44 +00:00
Benjamin S. Leveritt
02c1ec63cc Change cloud keys and peer addresses 2024-12-05 16:54:33 +00:00
Benjamin S. Leveritt
df797dedcd Change email addresses 2024-12-05 16:53:01 +00:00
Benjamin S. Leveritt
74cb08e7d4 Replaces github url with new one 2024-12-05 16:48:44 +00:00
Anselm Eickhoff
73720a8cc4 Merge pull request #907 from garden-co/latency-map
Add a latency map to cloud page
2024-12-05 16:36:39 +00:00
Anselm Eickhoff
50c77fa788 Merge pull request #916 from garden-co/docs/examples-demo
Show demo and code for minimal examples
2024-12-05 16:35:04 +00:00
Benjamin S. Leveritt
9977a4ee85 Use jsxEmit value rather than string 2024-12-05 16:06:46 +00:00
Guido D'Orsi
441fe27802 chore: changeset 2024-12-05 15:53:19 +01:00
Marina Orlova
1afbd2c7cc Add changeset 2024-12-05 15:43:12 +01:00
Marina Orlova
4955e39af5 Create cojson-storage 2024-12-05 15:43:12 +01:00
Marina Orlova
ace151696c Copy syncUtils code into sqlite package 2024-12-05 15:43:12 +01:00
Marina Orlova
db9560ebc5 Fix ERR_MODULE_NOT_FOUND 2024-12-05 15:43:12 +01:00
Marina Orlova
80b572710e Fix tests 2024-12-05 15:43:12 +01:00
Marina Orlova
bbb9d45969 Unify IDB and SQLite storage code 2024-12-05 15:43:12 +01:00
Marina Orlova
83e9a3eaa8 Normalise sqlite code against indexedb 2024-12-05 15:43:12 +01:00
Marina Orlova
9ff7e68f7d Split sqlite storage 2024-12-05 15:43:12 +01:00
Anselm Eickhoff
06740e840a Merge pull request #927 from garden-co/jazz-551-optimise-large-record-like-comaps-for-access-of-latest-value
Optimise large record-like CoMaps for access of latest value
2024-12-05 10:28:19 +00:00
Guido D'Orsi
b0e2c4fd4b fix: revert the incremental processing optimization as it doesn't take into account that older transactions might be synced after the new ones 2024-12-05 11:11:49 +01:00
Guido D'Orsi
4c1922c10e perf(coMap): process only the new transactions on update 2024-12-05 11:03:35 +01:00
Anselm Eickhoff
76bad9b5c3 Merge pull request #922 from garden-co/jazz-547-explicitly-reference-homepage-dependencies
Explicitly reference homepage dependencies
2024-12-05 09:46:55 +00:00
Trisha Lim
7f16f2705e Attempt fix build 2024-12-04 21:54:08 +00:00
Guido D'Orsi
d12594e521 fix: fix timing issues and invalidate on update 2024-12-04 21:13:43 +01:00
Guido D'Orsi
cf96350b01 Merge remote-tracking branch 'origin/main' into jazz-551-optimise-large-record-like-comaps-for-access-of-latest-value 2024-12-04 20:21:53 +01:00
Anselm
947030433f Merge branch 'main' into latency-map 2024-12-04 18:27:34 +00:00
Anselm
b4cebd732e Merge branch 'main' into docs/examples-demo 2024-12-04 18:26:40 +00:00
Anselm
d0d95e6d5d Merge branch 'main' into jazz-547-explicitly-reference-homepage-dependencies 2024-12-04 18:23:47 +00:00
Trisha Lim
a75383ac6a Fix build 2024-12-04 18:15:44 +00:00
Trisha Lim
de503b6120 Formatting fixes 2024-12-04 18:07:59 +00:00
Trisha Lim
f7ae41254f Merge branch 'main' into docs/examples-demo 2024-12-04 18:06:09 +00:00
Trisha Lim
8f348b28c6 Improve code example styling 2024-12-04 18:05:53 +00:00
Trisha Lim
3b185b4cd3 Merge branch 'main' into latency-map 2024-12-04 16:54:50 +00:00
Anselm
df5dc513bf Don't reverse iterate over valid transactions for now 2024-12-04 16:51:11 +00:00
Benjamin S. Leveritt
8e6783ad88 Link dependant jazz packages for api docs generation 2024-12-04 16:48:59 +00:00
Benjamin S. Leveritt
79bf6f478f Add Turbo build config to homepage workspace 2024-12-04 16:48:59 +00:00
Trisha Lim
7fd93a5a61 Move Examples page before API Ref nav link 2024-12-04 16:48:39 +00:00
Trisha Lim
8f9687323f Reorder examples 2024-12-04 16:48:39 +00:00
Trisha Lim
47ee25786f Spacing 2024-12-04 16:48:39 +00:00
Trisha Lim
0009aa19b2 Example demo header design 2024-12-04 16:48:39 +00:00
Trisha Lim
741b9cbada Move components out of page.tsx 2024-12-04 16:48:39 +00:00
Trisha Lim
259ade3099 Consistent layout and styling in demo apps 2024-12-04 16:48:39 +00:00
Trisha Lim
b6f2da2221 Move demos to separate section 2024-12-04 16:48:39 +00:00
Trisha Lim
44157945a0 Show code samples 2024-12-04 16:48:39 +00:00
Trisha Lim
4ff7bb500a Show iframe for examples 2024-12-04 16:48:39 +00:00
Anselm
db5ea54338 Fix jazz-tools use of _raw.ops 2024-12-04 16:26:19 +00:00
Anselm
7c7880a9b2 Optimise large record-like CoMaps for access of latest value 2024-12-04 16:21:01 +00:00
Trisha Lim
49082a5aad Merge branch 'main' into latency-map 2024-12-04 10:41:18 +00:00
Trisha Lim
b6653555f5 Fix build 2024-12-03 15:45:24 +00:00
Trisha Lim
c9fd16ce21 Desktop view 2024-12-03 14:46:14 +00:00
Trisha Lim
e60f34d9e6 Mobile view 2024-12-03 14:28:24 +00:00
Trisha Lim
a04c7dca7a Install next-themes 2024-12-03 12:37:00 +00:00
Trisha Lim
e067c29d81 Map positioning 2024-12-03 12:36:14 +00:00
Trisha Lim
1357306d1b Switch latency map colors according to theme 2024-12-03 12:10:22 +00:00
Trisha Lim
3c6d9b20c1 Move theme controls to gcmp/jazz, and out of design system 2024-12-03 12:03:22 +00:00
Trisha Lim
f597316267 Remove console logs 2024-12-03 11:18:50 +00:00
Trisha Lim
17f8bc25c3 Update colors for dark mode 2024-12-02 20:21:29 +00:00
Trisha Lim
c1d652cf7f Update colors for light mode 2024-12-02 20:05:38 +00:00
Trisha Lim
10f3e4aabd Update colors for light mode 2024-12-02 17:24:42 +00:00
Anselm
63f5574003 Improve map 2024-12-02 16:26:38 +00:00
Anselm
9bb5c4ca5f Add a latency map to cloud page 2024-12-01 16:03:38 +00:00
116 changed files with 24837 additions and 1366 deletions

View File

@@ -0,0 +1,6 @@
---
"cojson": patch
"jazz-tools": patch
---
Optimise large record-like CoMaps for access of latest value

View File

@@ -0,0 +1,7 @@
---
"cojson-storage-indexeddb": patch
"cojson-storage-sqlite": patch
"cojson-storage": patch
---
Refactor the SQLite and IndexedDB storage packages to extract common synchronization functionality into newly created cojson-storage package.

View File

@@ -59,7 +59,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to [the community leaders responsible for enforcement](mailto:hello@gcmp.io).
reported to [the community leaders responsible for enforcement](mailto:hello@garden.co).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -6,7 +6,7 @@ Thank you for considering contributing to Jazz! Jazz is an open-source framework
### 1. Reporting Bugs
If you find a bug, please [open an issue with as much detail as possible](https://github.com/gardencmp/jazz/issues). Include:
If you find a bug, please [open an issue with as much detail as possible](https://github.com/garden-co/jazz/issues). Include:
- A clear and descriptive title.
- Steps to reproduce the issue.
@@ -40,7 +40,7 @@ You'll need Node.js 20.x or 22.x installed (we're working on support for 23.x),
1. **Clone the repository**:
```bash
git clone https://github.com/gardencmp/jazz.git
git clone https://github.com/garden-co/jazz.git
```
2. **Install dependencies**:

View File

@@ -15,6 +15,6 @@ For community and support, please join our [Discord](https://discord.gg/utDMjHYg
- Homepage: [jazz.tools](https://jazz.tools)
- Docs: [jazz.tools/docs](https://jazz.tools/docs)
- Community & support: [Discord](https://discord.gg/utDMjHYg42)
- Updates: [X](https://x.com/jazz_tools) & [Email](https://gcmp.io/news)
- Updates: [X](https://x.com/jazz_tools) & [Email](https://garden.co/news)
Copyright 2024 — Garden Computing, Inc.

View File

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

View File

@@ -1,11 +1,11 @@
import { useClerk, useUser } from "@clerk/clerk-expo";
import { useJazzClerkAuth } from "jazz-react-auth-clerk";
import React, {
useContext,
createContext,
PropsWithChildren,
useContext,
useEffect,
useState,
PropsWithChildren,
} from "react";
import { Text, View } from "react-native";
import { Jazz } from "./jazz";
@@ -49,7 +49,7 @@ export function JazzAndAuth({ children }: PropsWithChildren) {
{auth ? (
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=chat-rn-clerk-example-jazz@gcmp.io"
peer="wss://cloud.jazz.tools/?key=chat-rn-clerk-example-jazz@garden.co"
storage={undefined}
>
{children}

View File

@@ -51,7 +51,7 @@ function App() {
<StrictMode>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=chat-rn-example-jazz@gcmp.io"
peer="wss://cloud.jazz.tools/?key=chat-rn-example-jazz@garden.co"
storage={undefined}
>
<NavigationContainer linking={linking} ref={navigationRef}>

View File

@@ -1,7 +1,7 @@
import "./index.css";
import { DemoAuthBasicUI, createJazzVueApp, useDemoAuth } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import "./index.css";
import router from "./router";
const Jazz = createJazzVueApp();
@@ -18,7 +18,7 @@ const RootComponent = defineComponent({
JazzProvider,
{
auth: authMethod.value,
peer: "wss://mesh.jazz.tools/?key=chat-example-jazz@gcmp.io",
peer: "wss://cloud.jazz.tools/?key=chat-example-jazz@garden.co",
},
{
default: () => h(App),

View File

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

View File

@@ -13,7 +13,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=chat-example-jazz@gcmp.io"
peer="wss://cloud.jazz.tools/?key=chat-example-jazz@garden.co"
>
{children}
</Jazz.Provider>

View File

@@ -28,7 +28,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
{clerk.user && auth ? (
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=minimal-auth-clerk-example@gcmp.io"
peer="wss://cloud.jazz.tools/?key=minimal-auth-clerk-example@garden.co"
>
{children}
</Jazz.Provider>

View File

@@ -5,17 +5,19 @@ function App() {
const { me, logOut } = useAccount();
return (
<div className="container">
<nav>
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button onClick={() => logOut()}>Logout</button>
</nav>
<main>
<>
<header>
<nav className="container">
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button onClick={() => logOut()}>Log out</button>
</nav>
</header>
<main className="container">
<ImageUpload />
</main>
</div>
</>
);
}

View File

@@ -11,6 +11,7 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--border-color: #2f2e2d;
}
html,
@@ -19,6 +20,10 @@ body,
height: 100%;
}
body {
margin: 0;
}
button {
border-radius: 8px;
border: 0;
@@ -30,6 +35,7 @@ button {
@media (prefers-color-scheme: light) {
:root {
--border-color: #e5e5e5;
color: #213547;
background-color: #ffffff;
}
@@ -43,23 +49,32 @@ button {
}
}
* {
border-color: var(--border-color);
}
header,
main {
padding: 0.5rem 0;
}
header {
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid whitesmoke;
margin-bottom: 2rem;
}
.container {
max-width: 500px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 0.75rem;
padding-left: 0.75rem;
max-width: 800px;
}
label {

View File

@@ -18,11 +18,14 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=image-upload-example@gcmp.io"
peer="wss://cloud.jazz.tools/?key=image-upload-example@garden.co"
>
{children}
</Jazz.Provider>
<DemoAuthBasicUI appName="Image upload" state={authState} />
{authState.state !== "signedIn" && (
<DemoAuthBasicUI appName="Image upload" state={authState} />
)}
</>
);
}

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
(new URL(window.location.href).searchParams.get(
"peer",
) as `ws://${string}`) ??
"wss://cloud.jazz.tools/?key=music-player-example-jazz@gcmp.io";
"wss://cloud.jazz.tools/?key=music-player-example-jazz@garden.co";
return (
<>

View File

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

View File

@@ -1,9 +1,9 @@
import App from "@/App.tsx";
import React from "react";
import ReactDOM from "react-dom/client";
import "@/index.css";
import { HRAccount } from "@/schema.ts";
import { DemoAuthBasicUI, createJazzReactApp, useDemoAuth } from "jazz-react";
import React from "react";
import ReactDOM from "react-dom/client";
const Jazz = createJazzReactApp({
AccountSchema: HRAccount,
@@ -14,7 +14,7 @@ const peer =
(new URL(window.location.href).searchParams.get(
"peer",
) as `ws://${string}`) ??
"wss://cloud.jazz.tools/?key=onboarding-example-jazz@gcmp.io";
"wss://cloud.jazz.tools/?key=onboarding-example-jazz@garden.co";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, authState] = useDemoAuth();

View File

@@ -17,7 +17,7 @@
{#if auth.current}
<Provider
auth={auth.current}
peer="wss://cloud.jazz.tools/?key=minimal-svelte-auth-passkey@gcmp.io"
peer="wss://cloud.jazz.tools/?key=minimal-svelte-auth-passkey@garden.co"
>
{@render children?.()}
</Provider>

View File

@@ -21,7 +21,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=minimal-auth-passkey-example@gcmp.io"
peer="wss://cloud.jazz.tools/?key=minimal-auth-passkey-example@garden.co"
>
{children}
</Jazz.Provider>

View File

@@ -1,13 +1,13 @@
import ReactDOM from "react-dom/client";
import App from "./5_App.tsx";
import "./index.css";
import {
PasskeyAuthBasicUI,
createJazzReactApp,
usePasskeyAuth,
} from "jazz-react";
import React from "react";
import ReactDOM from "react-dom/client";
import { PasswordManagerAccount } from "./1_schema.ts";
import App from "./5_App.tsx";
import "./index.css";
const Jazz = createJazzReactApp<PasswordManagerAccount>({
AccountSchema: PasswordManagerAccount,
@@ -24,7 +24,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=password-manager-example-jazz@gcmp.io"
peer="wss://cloud.jazz.tools/?key=password-manager-example-jazz@garden.co"
>
{children}
</Jazz.Provider>

View File

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

View File

@@ -23,7 +23,7 @@ const peer =
(new URL(window.location.href).searchParams.get(
"peer",
) as `ws://${string}`) ??
"wss://cloud.jazz.tools/?key=pets-example-jazz@gcmp.io";
"wss://cloud.jazz.tools/?key=pets-example-jazz@garden.co";
/** Walkthrough: The top-level provider `<Jazz.Provider/>`
*

View File

@@ -17,21 +17,27 @@ function App() {
};
return (
<div className="container">
<nav>
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button className="btn" onClick={() => logOut()}>
Logout
</button>
</nav>
<>
<header>
<nav className="container">
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button className="btn" onClick={() => logOut()}>
Log out
</button>
</nav>
</header>
{router.route({
"/": () => createReactions() as never,
"/reactions/:id": (id) => <ReactionsScreen id={id as ID<Reactions>} />,
})}
</div>
<main className="container">
{router.route({
"/": () => createReactions() as never,
"/reactions/:id": (id) => (
<ReactionsScreen id={id as ID<Reactions>} />
),
})}
</main>
</>
);
}

View File

@@ -5,13 +5,27 @@
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #000;
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--border-color: #404040;
--border-color: #2f2e2d;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}
button {
cursor: pointer;
}
button.btn {
@@ -43,29 +57,28 @@ button.btn {
border-color: var(--border-color);
}
html,
body,
#root {
height: 100%;
header,
main {
padding: 0.5rem 0;
}
header {
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.container {
max-width: 500px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 0.75rem;
padding-left: 0.75rem;
max-width: 800px;
}
h1,

View File

@@ -15,11 +15,14 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=reactions-example@gcmp.io"
peer="wss://cloud.jazz.tools/?key=reactions-example@garden.co"
>
{children}
</Jazz.Provider>
<DemoAuthBasicUI appName="Reactions" state={authState} />
{authState.state !== "signedIn" && (
<DemoAuthBasicUI appName="Reactions" state={authState} />
)}
</>
);
}

View File

@@ -1,7 +1,7 @@
import "./assets/main.css";
import { DemoAuthBasicUI, createJazzVueApp, useDemoAuth } from "jazz-vue";
import { createApp, defineComponent, h } from "vue";
import App from "./App.vue";
import "./assets/main.css";
import router from "./router";
import { ToDoAccount } from "./schema";
@@ -19,7 +19,7 @@ const RootComponent = defineComponent({
JazzProvider,
{
auth: authMethod.value,
peer: "wss://mesh.jazz.tools/?key=vue-todo-example-jazz@gcmp.io",
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
{
default: () => h(App),

View File

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

View File

@@ -48,7 +48,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<>
<Jazz.Provider
auth={passkeyAuth}
peer="wss://cloud.jazz.tools/?key=todo-example-jazz@gcmp.io"
peer="wss://cloud.jazz.tools/?key=todo-example-jazz@garden.co"
>
{children}
</Jazz.Provider>

View File

@@ -76,6 +76,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
<ButtonIcon icon={icon} loading={loading} />
{children}
{newTab ? (
<span className="inline-block text-stone-300 dark:text-stone-700 relative -top-0.5 -left-2 -mr-2">
</span>
) : (
""
)}
</Link>
);
}

View File

@@ -7,22 +7,30 @@ export function GappedGrid({
className,
title,
cols = 3,
gap = "md",
}: {
children: ReactNode;
className?: string;
title?: string;
cols?: 3 | 4;
gap?: "none" | "md";
}) {
const colsClassName =
cols === 3
? "grid-cols-2 md:grid-cols-4 lg:grid-cols-6"
: "sm:grid-cols-2 lg:grid-cols-4";
const gapClassName = {
none: "gap-0",
md: "gap-4 lg:gap-8",
}[gap];
return (
<div
className={clsx(
"grid gap-4 lg:gap-8",
"grid",
colsClassName,
gapClassName,
"items-stretch",
className,
)}

View File

@@ -3,10 +3,14 @@
import clsx from "clsx";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { UseThemeProps } from "next-themes/dist/types";
import { useEffect, useState } from "react";
export function ThemeToggle({ className }: { className?: string }) {
let { resolvedTheme, setTheme } = useTheme();
export function ThemeToggle({
className,
resolvedTheme,
setTheme,
}: { className?: string } & UseThemeProps) {
let otherTheme = resolvedTheme === "dark" ? "light" : "dark";
let [mounted, setMounted] = useState(false);

View File

@@ -1,13 +1,7 @@
"use client";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
import { UseThemeProps } from "next-themes/dist/types";
import { useEffect } from "react";
function ThemeWatcher() {
let { resolvedTheme, setTheme } = useTheme();
export function ThemeWatcher({ resolvedTheme, setTheme }: UseThemeProps) {
useEffect(() => {
let media = window.matchMedia("(prefers-color-scheme: dark)");
@@ -28,12 +22,3 @@ function ThemeWatcher() {
return null;
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<ThemeWatcher />
{children}
</NextThemesProvider>
);
}

View File

@@ -3,8 +3,7 @@
import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode } from "react";
import { ThemeToggle } from "../molecules/ThemeToggle";
import { ComponentType, ReactNode } from "react";
import { NewsletterForm } from "./NewsletterForm";
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
@@ -22,6 +21,7 @@ type FooterProps = {
companyName: string;
sections: FooterSection[];
socials: SocialLinksProps;
themeToggle: ComponentType<{ className?: string }>;
};
function Copyright({
@@ -38,7 +38,13 @@ function Copyright({
);
}
export function Footer({ logo, companyName, sections, socials }: FooterProps) {
export function Footer({
logo,
companyName,
sections,
socials,
themeToggle: ThemeToggle,
}: FooterProps) {
return (
<footer className="w-full py-8 mt-12 md:mt-20">
<div className="container grid gap-8 md:gap-12">

View File

@@ -11,9 +11,15 @@ import clsx from "clsx";
import { ChevronDownIcon, MenuIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
import {
ComponentType,
ReactNode,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { BreadCrumb } from "../molecules/Breadcrumb";
import { ThemeToggle } from "../molecules/ThemeToggle";
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
type NavItemProps = {
@@ -32,6 +38,7 @@ type NavProps = {
docNav?: ReactNode;
cta?: ReactNode;
socials?: SocialLinksProps;
themeToggle: ComponentType<{ className?: string }>;
};
function NavItem({
@@ -112,7 +119,14 @@ function NavItem({
);
}
export function MobileNav({ mainLogo, items, docNav, cta, socials }: NavProps) {
export function MobileNav({
mainLogo,
items,
docNav,
cta,
socials,
themeToggle: ThemeToggle,
}: NavProps) {
const [menuOpen, setMenuOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null);

View File

@@ -1,14 +1,14 @@
import "./globals.css";
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
import { ThemeProvider } from "@/components/ThemeProvider";
import type { Metadata } from "next";
import "./globals.css";
import { Inter, Manrope } from "next/font/google";
import localFont from "next/font/local";
import { GcmpNav } from "@/components/Nav";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { ThemeToggle } from "gcmp-design-system/src/app/components/molecules/ThemeToggle";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
@@ -44,7 +44,7 @@ const metaTags = {
title: "garden computing",
description:
"Computers are magic. So why do we put up with so much complexity? We believe just a few new ideas can make all the difference.",
url: "https://gcmp.io",
url: "https://garden.co",
};
export const metadata: Metadata = {

View File

@@ -20,7 +20,7 @@ export default function NewsPage() {
<NewsletterForm />
<p>
Follow us on <Link href="https://x.com/gcmp_io">@gcmp.io</Link> or{" "}
Follow us on <Link href="https://x.com/gcmp_io">@garden.co</Link> or{" "}
<Link href="https://x.com/jazz_tools">@jazz_tools</Link>.
</p>

View File

@@ -1,9 +1,14 @@
import { ThemeToggle } from "@/components/ThemeToggle";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
import { GcmpLogo } from "gcmp-design-system/src/app/components/atoms/logos/GcmpLogo";
import { Nav } from "gcmp-design-system/src/app/components/organisms/Nav";
export function GcmpNav() {
const cta = (
<Button variant="secondary" className="ml-auto" href="mailto:hello@gcmp.io">
<Button
variant="secondary"
className="ml-auto"
href="mailto:hello@garden.co"
>
Contact us
</Button>
);
@@ -16,6 +21,7 @@ export function GcmpNav() {
{ title: "Team", href: "/team" },
]}
cta={cta}
themeToggle={ThemeToggle}
></Nav>
);
}

View File

@@ -0,0 +1,16 @@
"use client";
import { ThemeWatcher } from "gcmp-design-system/src/app/components/molecules/ThemeWatcher";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const useThemeProps = useTheme();
return (
<NextThemesProvider {...props}>
<ThemeWatcher {...useThemeProps} />
{children}
</NextThemesProvider>
);
}

View File

@@ -0,0 +1,10 @@
"use client";
import { ThemeToggle as GardenThemeToggle } from "gcmp-design-system/src/app/components/molecules/ThemeToggle";
import { useTheme } from "next-themes";
export function ThemeToggle({ className }: { className?: string }) {
let useThemeProps = useTheme();
return <GardenThemeToggle className={className} {...useThemeProps} />;
}

View File

@@ -26,6 +26,7 @@
"mdast-util-mdx": "^3.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"next": "14.2.15",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"shiki": "^0.14.6",

View File

@@ -1,4 +1,5 @@
import { PackageDocs } from "@/components/docs/packageDocs";
import { requestProject } from "@/components/docs/requestProject";
import { packages } from "@/lib/packages";
import { notFound } from "next/navigation";
@@ -6,12 +7,14 @@ interface Props {
params: { package: string };
}
export default function Page({ params }: Props) {
export default async function Page({ params }: Props) {
if (!packages.map((p) => p.name).includes(params.package)) {
return notFound();
}
return <PackageDocs package={params.package} />;
const project = await requestProject(params.package as any);
return <PackageDocs project={project} package={params.package} />;
}
export async function generateMetadata({ params }: Props) {

View File

@@ -1,5 +1,6 @@
import { ApiNav } from "@/components/docs/ApiNav";
import DocsLayout from "@/components/docs/DocsLayout";
import { requestProjects } from "@/components/docs/requestProject";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export const metadata = {
@@ -8,13 +9,15 @@ export const metadata = {
"API references for packages like jazz-tools, jazz-react, and more.",
};
export default function Layout({
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const projects = await requestProjects();
return (
<DocsLayout nav={<ApiNav />}>
<DocsLayout nav={<ApiNav projects={projects} />}>
<Prose className="py-8 [&_*]:scroll-mt-[8rem]">{children}</Prose>
</DocsLayout>
);

View File

@@ -3,8 +3,6 @@ import { clsx } from "clsx";
import { MessageCircleQuestionIcon, PackageIcon } from "lucide-react";
import Link from "next/link";
import { DocNav } from "@/components/docs/nav";
const CardHeading = ({
children,
className,
@@ -95,7 +93,7 @@ export default function Page() {
</Link>
, or open an issue on{" "}
<Link
href="https://github.com/gardencmp/jazz"
href="https://github.com/garden-co/jazz"
className="underline"
>
GitHub

View File

@@ -1,6 +1,6 @@
#### Jazz Cloud + Data Backup Node
<p className="no-prose text-base">
<span className="no-prose text-base">
Connect your users to Jazz Cloud for all its benefits, but also run and
connect your own data backup node.
</p>
</span>

View File

@@ -1,6 +1,6 @@
#### Jazz Cloud + DIY Cloud
<p className="no-prose text-base">
<span className="no-prose text-base">
Connect your users to Jazz Cloud, or your own nodes as a lower-performance
fallback. The two networks stay in constant sync.
</p>
</span>

View File

@@ -1,5 +1,6 @@
#### Completely DIY Cloud
<p className="no-prose text-base">
<span className="no-prose text-base">
Build your own network of sync and storage nodes. Handle
devops,networking, security and backups yourself.
</p>
</span>

View File

@@ -1,4 +1,5 @@
import { Pricing } from "@/components/Pricing";
import { LatencyMap } from "@/components/cloud/latencyMap";
import { GridCard } from "gcmp-design-system/src/app/components/atoms/GridCard";
import {
H2,
@@ -24,12 +25,12 @@ export const metadata = {
export default function Cloud() {
return (
<div className="space-y-16">
<HeroHeader
className="container"
title="Jazz Cloud"
slogan="Real-time sync and storage infrastructure that scales up to millions of users."
/>
<div className="container">
<div className="container space-y-12 overflow-x-hidden sm:overflow-x-visible">
<HeroHeader
title="Jazz Cloud"
slogan="Real-time sync and storage infrastructure that scales up to millions of users."
/>
<LatencyMap />
<GappedGrid>
<GridCard>
<H3>Optimal cloud routing</H3>
@@ -66,69 +67,6 @@ export default function Cloud() {
</div>
<div className="container space-y-16">
<div>
<SectionHeader
title="Global footprint"
slogan="We're rapidly expanding our network of sync & storage nodes. This is our current coverage."
/>
<GappedGrid>
<div className="text-sm">
<H4>Under 50ms RTT</H4>
<UL>
<LI>Frankfurt</LI>
<LI>New York</LI>
<LI>Newark</LI>
<LI>North California</LI>
<LI>North Virginia</LI>
<LI>San Francisco</LI>
<LI>Singapore</LI>
<LI>Toronto</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 100ms RTT</H4>
<UL>
<LI>Amsterdam</LI>
<LI>Atlanta</LI>
<LI>London</LI>
<LI>Ohio</LI>
<LI>Paris</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 200ms RTT</H4>
<UL>
<LI>Bangalore</LI>
<LI>Dallas</LI>
<LI>Mumbai</LI>
<LI>Oregon</LI>
</UL>
<H4>Under 300ms RTT</H4>
<UL>
<LI> Seoul</LI>
<LI> Tokyo</LI>
</UL>
</div>
<div className="text-sm">
<H4>Under 400ms RTT</H4>
<UL>
<LI>Sao Paulo</LI>
<LI>Sydney</LI>
</UL>
<H4>Under 500ms RTT</H4>
<UL>
<LI>Cape Town</LI>
</UL>
</div>
</GappedGrid>
</div>
<div>
<SectionHeader
title="Custom deployment scenarios"

View File

@@ -207,8 +207,8 @@ You can optionally pass a custom `kvStore` and `AccountSchema` to `createJazzRNA
Refer to the Jazz + React Native demo projects for implementing authentication:
- [DemoAuth Example](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn)
- [ClerkAuth Example](https://github.com/gardencmp/jazz/tree/main/examples/chat-rn-clerk)
- [DemoAuth Example](https://github.com/garden-co/jazz/tree/main/examples/chat-rn)
- [ClerkAuth Example](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-clerk)
In the demos, you'll find details on:
@@ -219,7 +219,7 @@ In the demos, you'll find details on:
### Working with Images
To work with images in Jazz, import the `createImage` function from [`jazz-react-native-media-images`](https://github.com/gardencmp/jazz/tree/main/packages/jazz-react-native-media-images).
To work with images in Jazz, import the `createImage` function from [`jazz-react-native-media-images`](https://github.com/garden-co/jazz/tree/main/packages/jazz-react-native-media-images).
<CodeGroup>
```tsx
@@ -236,7 +236,7 @@ To work with images in Jazz, import the `createImage` function from [`jazz-react
```
</CodeGroup>
For a complete implementation, please refer to [this](https://github.com/gardencmp/jazz/blob/main/examples/pets/src/3_NewPetPostForm.tsx) demo.
For a complete implementation, please refer to [this](https://github.com/garden-co/jazz/blob/main/examples/pets/src/3_NewPetPostForm.tsx) demo.
### Running your app

View File

@@ -108,7 +108,7 @@ The easiest way to use Jazz with Next.JS is to only use it on the client side. Y
<>// old
<Jazz.Provider// old
auth={passkeyAuth}// old
peer="wss://mesh.jazz.tools/?key=you@example.com"// old
peer="wss://cloud.jazz.tools/?key=you@example.com"// old
>// old
{children}// old
</Jazz.Provider>// old

View File

@@ -4,7 +4,7 @@ import { CodeGroup } from "@/components/forMdx";
This guide provides step-by-step instructions for setting up and running a Jazz-powered Todo application using VueJS.
See the full example [here](https://github.com/gardencmp/jazz/tree/main/examples/todo-vue).
See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue).
---
@@ -127,7 +127,7 @@ const RootComponent = defineComponent({
JazzProvider,
{
auth: authMethod.value,
peer: "wss://mesh.jazz.tools/?key=vue-todo-example-jazz@gcmp.io",
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
{
default: () => h(App),
@@ -227,7 +227,7 @@ const folders = useCoState(FolderList, computedFoldersId, [{ items: [{}] }]);
```
</CodeGroup>
See the full example [here](https://github.com/gardencmp/jazz/tree/main/examples/todo-vue).
See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue).
## Mutating a CoValue
@@ -259,4 +259,4 @@ const createFolder = async (name: string) => {
```
</CodeGroup>
See the full example [here](https://github.com/gardencmp/jazz/tree/main/examples/todo-vue).
See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue).

View File

@@ -1,29 +1,32 @@
import { CodeGroup, ComingSoon } from "@/components/forMdx";
# CoValues
# Defining schemas: CoValues
**CoValues ("Collaborative Values") are the core abstraction of Jazz.** Think of them as bread-and-butter datastructures that you can use to represent everything you need in your app.
**CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app.
As their name suggests, they are inherently collaborative, meaning **multiple users and devices can edit them at the same time.**
As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.**
- CoValues allow for concurrent edits by always keeping their full edit histories, deriving the "current state" based on the currently locally available history.
- **Think of CoValues as "super-fast Git for lots of tiny data".**
- (The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type))
- Having the full history around also means that you often don't need explicit timestamps and author info - you get this for free, just by having different accounts edit a value and then looking at its [edit metadata](/docs/coming-soon).
**Think of CoValues as "super-fast Git for lots of tiny data."**
CoValues mostly model JSON with CoMaps and CoLists, but also offer CoStreams for simple per-user value streams, and also let you represent binary data with BinaryCoStreams.
- CoValues keep their full edit histories, from which they derive their "current state".
- The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type).
- Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's [edit metadata](/docs/coming-soon).
## Schemas as your app's first step
CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams.
Under the hood, CoValues are as dynamic and flexible as JSON itself, but the way you use them in Jazz is by defining fixed schemas to describe the shape of data in your app.
## Start your app with a schema
- This helps correctness and development speed in general, but is particularly important
- when you evolve your app and need migrations
- when different clients and server workers collaborate on CoValues and need to make compatible changes
Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app.
- Luckily, thinking about the shape of your data first is also a really good way to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
This helps correctness and development speed, but is particularly important...
- when you evolve your app and need migrations
- when different clients and server workers collaborate on CoValues and need to make compatible changes
Jazz makes it really quick to define and update schemas, since they are simple TypeScript classes:
Thinking about the shape of your data is also a great first step to model your app.
Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
Jazz makes it quick to declare schemas, since they are simple TypeScript classes:
<CodeGroup>
{/* prettier-ignore */}
@@ -35,14 +38,14 @@ export class TodoProject extends CoMap {
```
</CodeGroup>
Here you can see how we use the `co` definer for declaring collaboratively editable fields, which ensures that schema info is correct at the type level and at runtime.
Here you can see how we extend a CoValue type and use `co` for declaring (collaboratively) editable fields. This means that schema info is available for type inference *and* at runtime.
Classes might look a bit old-fashioned to some, but one nice property they have is that they are both types and values in TypeScript, so we can use them as both (with a single definition & import).
Classes might look old-fashioned, but Jazz makes use of them being both types and values in TypeScript, letting you refer to either with a single definition and import.
<CodeGroup>
{/* prettier-ignore */}
```ts
import { TodoProject } from "./schema";
import { TodoProject, ListOfTasks } from "./schema";
const project: TodoProject = TodoProject.create(
{
@@ -54,13 +57,154 @@ const project: TodoProject = TodoProject.create(
```
</CodeGroup>
## CoValue field types
## Types of CoValues
Before we look at the different types of CoValues, let's learn what we can put *into* them.
### `CoMap` (declaration)
CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects. (Collaborative editing follows a last-write-wins strategy per-key.)
You can either declare struct-like CoMaps:
<CodeGroup>
{/* prettier-ignore */}
```ts
class Person extends CoMap {
name = co.string;
age = co.number;
pet = co.optional.ref(Pet);
}
```
</CodeGroup>
Or record-like CoMaps (key-value pairs, where keys are always `string`):
<CodeGroup>
{/* prettier-ignore */}
```ts
class ColorToHex extends CoMap.Record(co.string) {}
class ColorToFruit extends CoMap.Record(co.ref(Fruit)) {}
```
</CodeGroup>
See the corresponding sections for [creating](/docs/using-covalues/creation#comap-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#comap-reading) and
[writing to](/docs/using-covalues/writing#comap-writing) CoMaps.
### `CoList` (declaration)
CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.)
You define them by specifying the type of the items they contain:
<CodeGroup>
{/* prettier-ignore */}
```ts
class ListOfColors extends CoList.Of(co.string) {}
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
```
</CodeGroup>
See the corresponding sections for [creating](/docs/using-covalues/creation#colist-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#colist-reading) and
[writing to](/docs/using-covalues/writing#colist-writing) CoLists.
### `CoFeed` (declaration)
CoFeeds are a special CoValue type that represent a feed of values for a set of users / sessions. (Each session of a user gets its own append-only feed.)
They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc.
You define them by specifying the type of feed item:
<CodeGroup>
{/* prettier-ignore */}
```ts
class FeedOfTasks extends CoFeed.Of(co.ref(Task)) {}
```
</CodeGroup>
See the corresponding sections for [creating](/docs/using-covalues/creation#cofeed-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#cofeed-reading) and
[writing to](/docs/using-covalues/writing#cofeed-writing) CoFeeds.
### `FileStream` (declaration)
FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.)
They allow you to upload and reference files, images, etc.
You typically don't need to declare or extend them yourself, you simply refer to the built-in `FileStream` from another CoValue:
<CodeGroup>
{/* prettier-ignore */}
```ts
import { FileStream } from "jazz-tools";
class UserProfile extends CoMap {
name = co.string;
avatar = co.ref(FileStream);
}
```
</CodeGroup>
See the corresponding sections for [creating](/docs/using-covalues/creation#filestream-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#filestream-reading) and
[writing to](/docs/using-covalues/writing#filestream-writing) FileStreams.
### `SchemaUnion` (declaration)
SchemaUnion is a helper type that allows you to load and refer to multiple subclasses of a CoMap schema, distinguished by a discriminating field.
You declare them with a base class type and discriminating lambda, in which you have access to the `RawCoMap`, on which you can call `get` with the field name to get the discriminating value.
<CodeGroup>
{/* prettier-ignore */}
```ts
import { SchemaUnion, CoMap } from "jazz-tools";
class BaseWidget extends CoMap {
type = co.string;
}
class ButtonWidget extends BaseWidget {
type = co.literal("button");
label = co.string;
}
class SliderWidget extends BaseWidget {
type = co.literal("slider");
min = co.number;
max = co.number;
}
const WidgetUnion = SchemaUnion.Of<BaseWidget>((raw) => {
switch (raw.get("type")) {
case "button": return ButtonWidget;
case "slider": return SliderWidget;
default: throw new Error("Unknown widget type");
}
});
```
</CodeGroup>
See the corresponding sections for [creating](/docs/using-covalues/creation#schemaunion-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading) and
[narrowing](/docs/using-covalues/reading#schemaunion-narrowing) SchemaUnions.
## CoValue field/item types
Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain.
### Primitive fields
You can define primitive field types using the `co` definer:
You can declare primitive field types using the `co` declarer:
<CodeGroup>
{/* prettier-ignore */}
@@ -89,7 +233,7 @@ co.literal("waiting", "ready");
```
</CodeGroup>
Finally, for more complex JSON data, that you *don't want to be collaborative internally* (but only every update as a whole), you can use `co.json<T>()`:
Finally, for more complex JSON data, that you *don't want to be collaborative internally* (but only ever update as a whole), you can use `co.json<T>()`:
<CodeGroup>
{/* prettier-ignore */}
@@ -98,7 +242,7 @@ co.json<{ name: string }>();
```
</CodeGroup>
For more detail, see the API Reference for the [`co` Field Definer](/api-reference/jazz-tools#co).
For more detail, see the API Reference for the [`co` field declarer](/api-reference/jazz-tools#co).
### Refs to other CoValues
@@ -108,7 +252,7 @@ Internally, this is represented by storing the IDs of the referenced CoValues in
The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in [Subscribing and Deep Loading](/docs/coming-soon).
In Schemas, you define Refs using the `co.ref<T>()` definer:
In Schemas, you declare Refs using the `co.ref<T>()` declarer:
<CodeGroup>
{/* prettier-ignore */}
@@ -123,7 +267,7 @@ class ListOfPeople extends CoList.Of(co.ref(Person)) {}
#### Optional Refs
If you want to make a referenced CoValue field optional, you *have to* use `co.optional.ref<T>()`:
⚠️ If you want to make a referenced CoValue field optional, you *have to* use `co.optional.ref<T>()`: ⚠️
<CodeGroup>
{/* prettier-ignore */}
@@ -134,6 +278,25 @@ class Person extends CoMap {
```
</CodeGroup>
### Computed fields, methods & constructors
### Computed fields & methods
<ComingSoon/>
Since CoValue schemas are based on classes, you can easily add computed fields and methods:
<CodeGroup>
{/* prettier-ignore */}
```ts
class Person extends CoMap {
firstName = co.string;
lastName = co.string;
dateOfBirth = co.Date;
get name() {
return `${this.firstName} ${this.lastName}`;
}
ageAsOf(date: Date) {
return differenceInYears(date, this.dateOfBirth);
}
}
```
</CodeGroup>

View File

@@ -44,4 +44,4 @@ In this case, provide the WebSocket endpoint your proxy exposes as the sync serv
### Source code
The implementation of this simple sync server is available open-source [on GitHub](https://github.com/gardencmp/jazz/blob/main/packages/jazz-run/src/startSync.ts).
The implementation of this simple sync server is available open-source [on GitHub](https://github.com/garden-co/jazz/blob/main/packages/jazz-run/src/startSync.ts).

View File

@@ -3,37 +3,22 @@ import { NextjsLogo } from "@/components/icons/NextjsLogo";
import { ReactLogo } from "@/components/icons/ReactLogo";
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
import { VueLogo } from "@/components/icons/VueLogo";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
import { H2 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { CloudUploadIcon, FingerprintIcon, ImageIcon } from "lucide-react";
type Example = {
name: string;
slug: string;
description?: string;
illustration?: React.ReactNode;
tech?: string[];
features?: string[];
demoUrl?: string;
};
const tech = {
react: "React",
nextjs: "Next.js",
reactNative: "React Native",
vue: "Vue",
};
const features = {
fileUpload: "File upload",
imageUpload: "Image upload",
passkey: "Passkey auth",
clerk: "Clerk auth",
inviteLink: "Invite link",
coFeed: "CoFeed",
};
import {
Schema_ts as ImageUploadSchema,
ImageUpload_tsx,
} from "@/codeSamples/examples/image-upload/src";
import {
Schema_ts as ReactionsSchema,
ReactionsScreen_tsx,
} from "@/codeSamples/examples/reactions/src";
import { ExampleCard } from "@/components/examples/ExampleCard";
import { ExampleDemo } from "@/components/examples/ExampleDemo";
import { Example, features, tech } from "@/lib/example";
const MockButton = ({ children }: { children: React.ReactNode }) => (
<p className="bg-blue-100 text-blue-800 py-1 p-2 rounded-full font-medium text-center text-xs">
@@ -206,7 +191,7 @@ const PasswordManagerIllustration = () => (
</div>
);
const reactExamples = [
const reactExamples: Example[] = [
{
name: "Chat",
slug: "chat",
@@ -215,53 +200,6 @@ const reactExamples = [
demoUrl: "https://chat.jazz.tools",
illustration: <ChatIllustration />,
},
{
name: "Clerk",
slug: "clerk",
description: "A React app that uses Clerk for authentication",
tech: [tech.react],
features: [features.clerk],
demoUrl: "https://clerk-demo.jazz.tools",
illustration: <ClerkIllustration />,
},
{
name: "Passkey",
slug: "passkey",
description: "A React app that uses Passkey for authentication",
tech: [tech.react],
features: [features.passkey],
demoUrl: "https://passkey-demo.jazz.tools",
illustration: (
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent">
<div className="p-4 flex flex-col items-center gap-3 rounded-md shadow-xl shadow-stone-400/20 bg-white dark:shadow-none">
<FingerprintIcon
size={36}
strokeWidth={0.75}
className="stroke-red-600"
/>
<p className="text-xs dark:text-stone-900">Continue with Touch ID</p>
</div>
</div>
),
},
{
name: "Image upload",
slug: "image-upload",
description: "Learn how to upload and delete images",
tech: [tech.react],
features: [features.imageUpload],
demoUrl: "https://image-upload-demo.jazz.tools",
illustration: <ImageUploadIllustration />,
},
{
name: "Reactions",
slug: "reactions",
description: "Collect and render reactions from multiple users.",
tech: [tech.react],
features: [features.coFeed],
demoUrl: "https://reactions-demo.jazz.tools",
illustration: <ReactionsIllustration />,
},
{
name: "Rate my pet",
slug: "pets",
@@ -302,9 +240,38 @@ const reactExamples = [
demoUrl: "https://music-demo.jazz.tools",
illustration: <MusicIllustration />,
},
{
name: "Clerk",
slug: "clerk",
description: "A React app that uses Clerk for authentication",
tech: [tech.react],
features: [features.clerk],
demoUrl: "https://clerk-demo.jazz.tools",
illustration: <ClerkIllustration />,
},
{
name: "Passkey",
slug: "passkey",
description: "A React app that uses Passkey for authentication",
tech: [tech.react],
features: [features.passkey],
demoUrl: "https://passkey-demo.jazz.tools",
illustration: (
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent">
<div className="p-4 flex flex-col items-center gap-3 rounded-md shadow-xl shadow-stone-400/20 bg-white dark:shadow-none">
<FingerprintIcon
size={36}
strokeWidth={0.75}
className="stroke-red-600"
/>
<p className="text-xs dark:text-stone-900">Continue with Touch ID</p>
</div>
</div>
),
},
];
const nextExamples = [
const nextExamples: Example[] = [
{
name: "Book shelf",
slug: "book-shelf",
@@ -358,6 +325,49 @@ const vueExamples: Example[] = [
},
];
const demos = [
{
name: "Image upload",
slug: "image-upload",
description: "Learn how to upload and delete images",
tech: [tech.react],
features: [features.imageUpload],
demoUrl: "https://image-upload-demo.jazz.tools",
illustration: <ImageUploadIllustration />,
showDemo: true,
codeSamples: [
{
name: "image-upload.tsx",
content: <ImageUpload_tsx />,
},
{
name: "schema.ts",
content: <ImageUploadSchema />,
},
],
},
{
name: "Reactions",
slug: "reactions",
description: "Collect and render reactions from multiple users.",
tech: [tech.react],
features: [features.coFeed],
demoUrl: "https://reactions-demo.jazz.tools",
illustration: <ReactionsIllustration />,
showDemo: true,
codeSamples: [
{
name: "reactions.tsx",
content: <ReactionsScreen_tsx />,
},
{
name: "schema.ts",
content: <ReactionsSchema />,
},
],
},
];
const categories = [
{
name: "React",
@@ -385,55 +395,6 @@ const categories = [
},
];
function Example({ example }: { example: Example }) {
const { name, slug, tech, features, description, demoUrl, illustration } =
example;
const githubUrl = `https://github.com/gardencmp/jazz/tree/main/examples/${slug}`;
return (
<div className="col-span-2 border bg-stone-50 shadow-sm p-3 flex flex-col rounded-lg dark:bg-stone-950">
<div className="mb-3 aspect-[16/9] overflow-hidden w-full rounded-md bg-white border dark:bg-stone-925 sm:aspect-[2/1] md:aspect-[3/2]">
{illustration}
</div>
<div className="flex-1 space-y-2 mb-2">
<h2 className="font-medium text-stone-900 dark:text-white leading-none">
{name}
</h2>
<div className="flex gap-1">
{tech?.map((tech) => (
<p
className="bg-green-50 border border-green-500 text-green-600 rounded-full py-0.5 px-2 text-xs dark:bg-green-800 dark:text-green-200 dark:border-green-700"
key={tech}
>
{tech}
</p>
))}
{features?.map((feature) => (
<p
className="bg-pink-50 border border-pink-500 text-pink-600 rounded-full py-0.5 px-2 text-xs dark:bg-pink-800 dark:text-pink-200 dark:border-pink-700"
key={feature}
>
{feature}
</p>
))}
</div>
<p className="text-sm">{description}</p>
</div>
<div className="flex gap-2">
<Button href={githubUrl} variant="secondary" size="sm">
View code
</Button>
{demoUrl && (
<Button href={demoUrl} variant="secondary" size="sm">
View demo
</Button>
)}
</div>
</div>
);
}
export default function Page() {
return (
<div className="container flex flex-col gap-6 pb-10 lg:pb-20">
@@ -442,6 +403,14 @@ export default function Page() {
slogan="Find an example app with code most similar to what you want to build"
/>
<div className="grid gap-8 mb-12 lg:gap-12">
<h2 className="sr-only">Example apps with demo and code</h2>
{demos.map(
(demo) =>
demo.showDemo && <ExampleDemo key={demo.slug} example={demo} />,
)}
</div>
<div className="grid gap-12 lg:gap-20">
{categories.map((category) => (
<div key={category.name}>
@@ -453,9 +422,17 @@ export default function Page() {
</div>
<GappedGrid>
{category.examples.map((example) => (
<Example key={example.slug} example={example} />
))}
{category.examples.map((example) =>
example.showDemo ? (
<ExampleDemo key={example.slug} example={example} />
) : (
<ExampleCard
className="border bg-stone-50 shadow-sm p-3 rounded-lg dark:bg-stone-950"
key={example.slug}
example={example}
/>
),
)}
</GappedGrid>
</div>
))}

View File

@@ -4,11 +4,11 @@ import type { Metadata } from "next";
import { Inter, Manrope } from "next/font/google";
import localFont from "next/font/local";
import { ThemeProvider } from "@/components/ThemeProvider";
import { JazzFooter } from "@/components/footer";
import { JazzNav } from "@/components/nav";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({

View File

@@ -79,7 +79,7 @@ pre.twoslash data-lsp:hover::before {
}
pre .code-container {
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-925 rounded-b-xl text-xs h-full;
@apply overflow-auto p-2 pl-0 bg-white dark:bg-stone-950 rounded-b-xl text-xs h-full leading-5;
}
/* The try button */
pre .code-container > a {

View File

@@ -0,0 +1,16 @@
"use client";
import { ThemeWatcher } from "gcmp-design-system/src/app/components/molecules/ThemeWatcher";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const useThemeProps = useTheme();
return (
<NextThemesProvider {...props}>
<ThemeWatcher {...useThemeProps} />
{children}
</NextThemesProvider>
);
}

View File

@@ -0,0 +1,10 @@
"use client";
import { ThemeToggle as GardenThemeToggle } from "gcmp-design-system/src/app/components/molecules/ThemeToggle";
import { useTheme } from "next-themes";
export function ThemeToggle({ className }: { className?: string }) {
let useThemeProps = useTheme();
return <GardenThemeToggle className={className} {...useThemeProps} />;
}

View File

@@ -0,0 +1,226 @@
"use client";
import React, { memo } from "react";
import { usePingColorThresholds } from "@/components/cloud/usePingColorThresholds";
import * as turf from "@turf/turf";
import type { FeatureCollection, Point, Position } from "geojson";
import MapTooltip from "./mapTooltip";
import land from "./ne_110m_land.json";
import { clsx } from "clsx";
// generated with: globalping ping cloud.jazz.tools from world --limit 500 --packets 16 --json | jq "del(.results[].result.rawOutput)" > pings.json
import pings from "./pings.json";
export const LatencyMap = () => {
const pingColorThresholds = usePingColorThresholds();
return (
<div className="relative mb-10 sm:-mt-5 -left-4 lg:-left-8 rounded-lg">
<MapSVG />
<MapTooltip />
<div className="absolute bottom-0 left-4 lg:bottom-8 lg:left-8 flex flex-col md:gap-1">
{pingColorThresholds.map((t, i) => (
<div
key={t.ping}
className={clsx("flex items-center gap-1", {
"hidden sm:flex": i % 2 !== 0,
})}
>
<div
className="size-2 md:size-3 rounded-full"
style={{ background: t.color }}
></div>
<div className="text-[9px] md:text-xs font-mono">
&lt;{t.ping}ms
</div>
</div>
))}
</div>
</div>
);
};
export const MapSVG = memo(({ spacing = 1.5 }: { spacing?: number }) => {
const pingColorThresholds = usePingColorThresholds();
// Define the data points with their latitudes, longitudes, and ping times
const serverLocations = [
{
city: "Los Angeles",
lat: 34.0522,
lng: -118.2437,
ip: "134.195.91.235",
color: "hsl(0, 50%, 50%)",
},
{
city: "New York",
lat: 40.7128,
lng: -74.006,
ip: "45.45.219.149",
color: "hsl(25, 50%, 50%)",
},
{
city: "London",
lat: 51.5074,
lng: -0.1278,
ip: "150.107.201.83",
color: "hsl(50, 50%, 50%)",
},
{
city: "Singapore",
lat: 1.3521,
lng: 103.8198,
ip: "103.214.23.227",
color: "hsl(250, 50%, 50%)",
},
{
city: "Sydney",
lat: -33.8688,
lng: 151.2153,
ip: "103.73.65.179",
color: "hsl(100, 50%, 50%)",
},
{
city: "Tokyo",
lat: 35.6895,
lng: 139.7014,
ip: "103.173.179.181",
color: "hsl(150, 50%, 50%)",
},
{
city: "Tel Aviv",
lat: 32.0853,
lng: 34.7818,
ip: "64.176.162.228",
color: "hsl(200, 50%, 50%)",
},
{
city: "Johannesburg",
lat: -26.2041,
lng: 28.0473,
ip: "139.84.228.42",
color: "hsl(225, 50%, 50%)",
},
{
city: "Vienna",
lat: 48.2085,
lng: 16.3721,
ip: "185.175.59.44",
color: "hsl(75, 50%, 50%)",
},
{
city: "Sao Paulo",
lat: -23.5505,
lng: -46.6333,
ip: "216.238.99.7",
color: "hsl(275, 50%, 50%)",
},
{
city: "Dallas",
lat: 32.7767,
lng: -96.797,
ip: "45.32.192.94",
color: "hsl(300, 50%, 50%)",
},
];
// create a grid of dots that are green if on land (contained in landOutlines) and blue if not
const extentX = 720;
const extentY = 160;
const grid = new Array(Math.round(extentX / spacing))
.fill(0)
.map((_, i) =>
new Array(Math.round(extentY / spacing))
.fill(0)
.map((_, j) => ({ x: i, y: j })),
);
// manually add Hawaii by lat/lng
grid.push([{ x: -155.844437, y: 19.8987 }]);
const dots = grid.flatMap((row) =>
row.map(({ x, y }) => ({
x: -450 + x * spacing + ((y % 2) * spacing) / 2,
y: -60 + y * spacing,
})),
);
const landPolygon = turf.multiPolygon(
land.geometries.map((g) => g.coordinates),
);
const dotsOnLand = turf.pointsWithinPolygon(
turf.points(dots.map((d) => [d.x, d.y])),
landPolygon,
) as FeatureCollection<Point>;
const scaleX = 3;
const scaleY = 3;
const offsetX = 600;
const offsetY = 260;
return (
<svg
viewBox="0 0 1200 440"
className="mx-auto"
dangerouslySetInnerHTML={{
__html: dotsOnLand.features
.map((dot, index) => {
const nearestMeasurement = pings.results.reduce(
(minDistance, ping) => {
if (
!ping.result.stats ||
ping.result.stats.rcv === 0 ||
ping.result.stats.avg === null
)
return minDistance;
const distance = turf.distance(
dot.geometry.coordinates,
[ping.probe.longitude, ping.probe.latitude],
{ units: "kilometers" },
);
const totalPing =
(2 * 1000 * distance) / (0.66 * 299_792) +
ping.result.stats.min;
if (distance < minDistance.dist) {
return {
city: ping.probe.city,
dist: distance,
ping: ping.result.stats.min,
totalPing,
resolvedAddress: ping.result.resolvedAddress,
};
}
return minDistance;
},
{
city: "",
dist: Infinity,
ping: Infinity,
totalPing: Infinity,
resolvedAddress: "",
},
);
return `<circle cx="${
dot.geometry.coordinates[0] * scaleX + offsetX
}" cy="${
-dot.geometry.coordinates[1] * scaleY + offsetY
}" r="${1.9 * spacing}" fill="${
pingColorThresholds.find(
(t) => nearestMeasurement.totalPing < t.ping,
)?.color
// serverLocations.find(
// (srv) => srv.ip == nearestMeasurement.resolvedAddress,
// )?.color
}" data-ping="${nearestMeasurement.totalPing.toFixed(
1,
)}ms" data-via="${nearestMeasurement.city + ""}" data-to="${
serverLocations.find(
(srv) => srv.ip == nearestMeasurement.resolvedAddress,
)?.city
}"/>`;
})
.join("\n"),
}}
/>
);
});

View File

@@ -0,0 +1,52 @@
"use client";
import { usePingColorThresholds } from "@/components/cloud/usePingColorThresholds";
import { useEffect } from "react";
export default function MapTooltip() {
const pingColorThresholds = usePingColorThresholds();
useEffect(() => {
// register callback for hovering, if we're over any circle, show the tooltip based on the data attributes
const onMouseMove = (e: MouseEvent) => {
const circ = e.target;
const el = document.querySelector(
".map-tooltip",
) as HTMLDivElement | null;
if (!el) return;
if (circ instanceof SVGCircleElement) {
const x = circ.cx.baseVal.value;
const y = circ.cy.baseVal.value;
const ping = parseInt(circ.dataset.ping || "0");
const via = circ.dataset.via;
const to = circ.dataset.to;
const text = `${ping}ms via ${via} to ${to}`;
el.style.display = "flex";
el.style.left = `calc(100% * ${x / 1400} + 30px)`;
el.style.top = `calc(100% * ${(y || 0) / 440} + 15px)`;
(el.children[0] as HTMLDivElement).style.backgroundColor =
pingColorThresholds.find((t) => t.ping >= ping)?.color || "";
(el.children[1] as HTMLDivElement).textContent = text;
} else {
el.style.display = "none";
}
};
document.addEventListener("mousemove", onMouseMove);
return () => {
document.removeEventListener("mousemove", onMouseMove);
};
}, []);
return (
<div className="map-tooltip absolute pointer-events-none text-xs bg-stone-925 text-stone-50 p-2 rounded-lg gap-1 items-center">
<div className="w-3 h-3 rounded-full"></div>
<div className="text-xs"></div>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
import { useTheme } from "next-themes";
export const pingColorThresholdsDark = [
{ ping: 5, color: "hsl(248, 50%, 100%)" },
{ ping: 10, color: "hsl(248, 50%, 80%)" },
{ ping: 15, color: "hsl(248, 50%, 72%)" },
{ ping: 25, color: "hsl(248, 50%, 62%)" },
{ ping: 35, color: "hsl(248, 50%, 54%)" },
{ ping: 45, color: "hsl(248, 50%, 49%)" },
{ ping: 55, color: "hsl(248, 50%, 43%)" },
{ ping: 65, color: "hsl(248, 50%, 39%)" },
{ ping: 100, color: "hsl(248, 50%, 35%)" },
{ ping: 150, color: "hsl(248, 50%, 28%)" },
{ ping: 200, color: "hsl(248, 50%, 23%)" },
{ ping: 300, color: "hsl(248, 50%, 20%)" },
{ ping: 1000, color: "hsl(248, 50%, 16%)" },
];
export const pingColorThresholdsLight = [
{ ping: 5, color: "hsl(260,100%,53%)" },
{ ping: 10, color: "hsl(258,95%,56%)" },
{ ping: 15, color: "hsl(256,93%,59%)" },
{ ping: 25, color: "hsl(252,90%,62%)" },
{ ping: 35, color: "hsl(250,88%,65%)" },
{ ping: 45, color: "hsl(245,87%,68%)" },
{ ping: 55, color: "hsl(240,86%,71%)" },
{ ping: 65, color: "hsl(238,84%,74%)" },
{ ping: 100, color: "hsl(235,80%,77%)" },
{ ping: 150, color: "hsl(232,73%,80%)" },
{ ping: 200, color: "hsl(230,69%,83%)" },
{ ping: 300, color: "hsl(230,65%,88%)" },
{ ping: 1000, color: "hsl(220,60%,92%)" },
];
export const usePingColorThresholds = () => {
const { resolvedTheme } = useTheme();
return resolvedTheme === "dark"
? pingColorThresholdsDark
: pingColorThresholdsLight;
};

View File

@@ -1,22 +1,24 @@
import { SideNav } from "@/components/SideNav";
import { SideNavHeader } from "@/components/SideNavHeader";
import { SideNavItem } from "@/components/SideNavItem";
import { docNavigationItems } from "@/lib/docNavigationItems";
import { packages } from "@/lib/packages";
import { clsx } from "clsx";
import { ChevronRight, PackageIcon } from "lucide-react";
import Link from "next/link";
import { twMerge } from "tailwind-merge";
import { requestProject } from "./requestProject";
import { ProjectReflection } from "typedoc";
export function ApiNav({
className,
projects,
}: { className?: string; projects: ProjectReflection[] }) {
if (!projects?.length) return;
export function ApiNav({ className }: { className?: string }) {
return (
<div className={clsx(className, "text-sm space-y-5")}>
<SideNavHeader href="/api-reference">API Reference</SideNavHeader>
<ul className="space-y-5">
{packages.map(({ name }) => (
<li key={name}>
<PackageNavItem package={name} />
{projects.map((project, index) => (
<li key={project.name}>
<PackageNavItem package={packages[index].name} project={project} />
</li>
))}
</ul>
@@ -24,13 +26,13 @@ export function ApiNav({ className }: { className?: string }) {
);
}
export async function PackageNavItem({
export function PackageNavItem({
package: packageName,
project,
}: {
package: string;
project: ProjectReflection;
}) {
let project = await requestProject(packageName as any);
return (
<>
<SideNavItem

View File

@@ -2,13 +2,13 @@ import { PackageIcon, Type } from "lucide-react";
import {
CommentDisplayPart,
DeclarationReflection,
ProjectReflection,
ReflectionKind,
SignatureReflection,
SomeType,
TypeContext,
TypeParameterReflection,
} from "typedoc";
import { requestProject } from "./requestProject";
import {
ClassOrInterface,
DocComment,
@@ -18,15 +18,13 @@ import {
PropDecl,
} from "./tags";
export async function PackageDocs({
export function PackageDocs({
package: packageName,
project,
}: {
package: string;
project: ProjectReflection;
}) {
let project = await requestProject(packageName as any);
// console.dir(project, {depth: 10});
return (
<>
<h2 className="flex items-center gap-2">

View File

@@ -21,3 +21,16 @@ export async function requestProject(
const deserializer = new Deserializer({} as any);
return deserializer.reviveProject(docs[packageName], packageName);
}
export async function requestProjects(): Promise<ProjectReflection[]> {
const projectNames = Object.keys(docs) as (keyof typeof docs)[];
const projects = await Promise.all(
projectNames.map(async (name) => {
const project = await requestProject(name);
return project;
}),
);
return projects;
}

View File

@@ -24,12 +24,12 @@ export function CodeExampleTabs({
return (
<div
className={clsx(
"bg-white h-[40rem] max-h-[80vh] flex flex-col",
"bg-white h-full flex flex-col",
"dark:bg-stone-925",
className,
)}
>
<div className="flex border-b overflow-x-auto overflow-y-hidden dark:bg-stone-900">
<div className="flex border-b overflow-x-auto overflow-y-hidden dark:bg-stone-950">
{tabs.map((tab, index) => (
<div key={index}>
<button
@@ -38,7 +38,7 @@ export function CodeExampleTabs({
activeTab === index
? "border-blue-700 bg-white text-stone-700 dark:bg-stone-925 dark:text-blue-500 dark:border-blue-500"
: "border-transparent text-stone-500 hover:text-stone-700 dark:hover:text-blue-500",
"flex items-center -mb-px transition-colors px-3 pb-1.5 pt-2 block text-xs border-b-2 ",
"flex items-center -mb-px transition-colors px-3 pb-2 pt-2.5 block text-xs border-b-2 ",
)}
onClick={() => setActiveTab(index)}
>

View File

@@ -0,0 +1,33 @@
import { ExampleLinks } from "@/components/examples/ExampleLinks";
import { ExampleTags } from "@/components/examples/ExampleTags";
import { Example } from "@/lib/example";
import { clsx } from "clsx";
export function ExampleCard({
example,
className,
}: { example: Example; className?: string }) {
const { name, description, illustration } = example;
return (
<div className={clsx(className, "col-span-2 flex flex-col")}>
{illustration && (
<div className="mb-3 aspect-[16/9] overflow-hidden w-full rounded-md bg-white border dark:bg-stone-925 sm:aspect-[2/1] md:aspect-[3/2]">
{illustration}
</div>
)}
<div className="flex-1 space-y-2 mb-2">
<h3 className="font-medium text-stone-900 dark:text-white leading-none">
{name}
</h3>
<ExampleTags example={example} />
<p className="text-sm">{description}</p>
</div>
<ExampleLinks example={example} />
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { CodeExampleTabs } from "@/components/examples/CodeExampleTabs";
import { ExampleLinks } from "@/components/examples/ExampleLinks";
import { ExampleTags } from "@/components/examples/ExampleTags";
import { Example } from "@/lib/example";
import { GappedGrid } from "gcmp-design-system/src/app/components/molecules/GappedGrid";
export function ExampleDemo({ example }: { example: Example }) {
const { name, demoUrl, illustration } = example;
return (
<GappedGrid
gap="none"
className="border bg-stone-50 shadow-sm rounded-lg dark:bg-stone-950 overflow-hidden"
>
<div className="p-3 col-span-full flex flex-col gap-2 justify-between items-baseline border-b sm:flex-row">
<div className="flex flex-col gap-2 items-baseline sm:flex-row">
<h2 className="font-medium text-stone-900 dark:text-white leading-none">
{name}
</h2>
<ExampleTags example={example} />
</div>
<ExampleLinks example={example} />
</div>
<div className="h-[25rem] lg:h-[30rem] border-t overflow-auto col-span-full md:col-span-2 lg:col-span-3 order-last md:order-none md:border-r md:border-t-0">
{example.codeSamples && (
<CodeExampleTabs tabs={example.codeSamples}></CodeExampleTabs>
)}
</div>
<div className="col-span-full md:p-8 md:col-span-2 lg:col-span-3 h-[25rem] lg:h-[30rem] lg:p-12">
<iframe
width="100%"
height="100%"
className="md:rounded-lg md:shadow-lg"
src={demoUrl}
title={name}
/>
</div>
</GappedGrid>
);
}

View File

@@ -0,0 +1,21 @@
import { Example } from "@/lib/example";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
export function ExampleLinks({ example }: { example: Example }) {
const { slug, demoUrl } = example;
const githubUrl = `https://github.com/gardencmp/jazz/tree/main/examples/${slug}`;
return (
<div className="flex gap-2">
<Button href={githubUrl} newTab variant="secondary" size="sm">
View code
</Button>
{demoUrl && (
<Button href={demoUrl} newTab variant="secondary" size="sm">
View demo
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { Example } from "@/lib/example";
export function ExampleTags({ example }: { example: Example }) {
const { tech, features } = example;
return (
<div className="flex gap-1">
{tech?.map((tech) => (
<p
className="bg-green-50 border border-green-500 text-green-600 rounded-full py-0.5 px-2 text-xs dark:bg-green-800 dark:text-green-200 dark:border-green-700"
key={tech}
>
{tech}
</p>
))}
{features?.map((feature) => (
<p
className="bg-pink-50 border border-pink-500 text-pink-600 rounded-full py-0.5 px-2 text-xs dark:bg-pink-800 dark:text-pink-200 dark:border-pink-700"
key={feature}
>
{feature}
</p>
))}
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { ThemeToggle } from "@/components/ThemeToggle";
import { socials } from "@/lib/socials";
import { GcmpLogo } from "gcmp-design-system/src/app/components/atoms/logos/GcmpLogo";
import { Footer } from "gcmp-design-system/src/app/components/organisms/Footer";
@@ -8,6 +9,7 @@ export function JazzFooter() {
logo={<GcmpLogo monochrome className="w-36" />}
companyName="Garden Computing, Inc."
socials={socials}
themeToggle={ThemeToggle}
sections={[
{
title: "About",
@@ -23,7 +25,7 @@ export function JazzFooter() {
newTab: true,
},
{
href: "https://github.com/gardencmp/jazz/releases",
href: "https://github.com/garden-co/jazz/releases",
label: "Releases",
newTab: true,
},

View File

@@ -1,7 +1,7 @@
import {
CodeExampleTabs as CodeExampleTabsClient,
CodeExampleTabsProps,
} from "@/components/CodeExampleTabs";
} from "@/components/examples/CodeExampleTabs";
import {
ContentByFramework as ContentByFrameworkClient,

View File

@@ -134,7 +134,7 @@ export function ChatDemoSection() {
slogan={
<>
A chat app in 174 lines of client-side code.{" "}
<Link href="https://github.com/gardencmp/jazz/tree/main/examples/chat">
<Link href="https://github.com/garden-co/jazz/tree/main/examples/chat">
View code
</Link>
</>

View File

@@ -16,6 +16,7 @@ import {
UserIcon,
} from "lucide-react";
import Link from "next/link";
import { MapSVG } from "../cloud/latencyMap";
const features = [
{
@@ -138,44 +139,51 @@ export function FeaturesSection() {
</Card>
))}
<div className="border p-4 sm:p-8 shadow-sm rounded-xl col-span-2 sm:col-span-4 space-y-5">
<H3>Jazz Cloud</H3>
<Prose className="max-w-xl">
<p>
Jazz Cloud is a real-time sync and storage infrastructure that
scales your Jazz app up to millions of users.{" "}
<strong>Instant setup, no configuration needed.</strong>
</p>
</Prose>
<ul className="flex flex-col sm:flex-row gap-4 text-sm">
{[
"Data & blob storage",
"Global sync",
"No limits for public alpha",
].map((feature) => (
<li
key={feature}
className="flex items-center gap-1.5 whitespace-nowrap"
>
<span className="text-blue p-1 rounded-full bg-blue-50 dark:text-blue-500 dark:bg-white/10">
<CheckIcon size={12} strokeWidth={3} />
</span>
{feature}
</li>
))}
</ul>
<div className="flex items-center flex-wrap gap-x-5 flex-wrap gap-y-3">
<Button href="/cloud" variant="primary">
View free tier & pricing
</Button>
<div className="relative border p-4 sm:p-8 shadow-sm rounded-xl col-span-2 sm:col-span-4 flex flex-col justify-end">
<div className="mb-3 sm:-right-3 sm:bottom-30 sm:absolute sm:left-[40%] sm:top-2 md:top-4 md:left-[23%]">
<MapSVG spacing={3} />
</div>
<H3>Jazz Cloud</H3>
<div className="relative z-10 space-y-4">
<Prose className="sm:max-w-[45%]" size="sm">
<p>
Jazz Cloud is real-time sync and storage infrastructure that
scales your Jazz app up to millions of users.{" "}
<strong>Instant setup, no config.</strong>
</p>
</Prose>
<div className="flex items-center flex-wrap gap-3">
<Button href="/cloud" variant="primary">
View free tier & pricing
</Button>
<Prose size="sm">
or{" "}
<Link href="/docs/sync-and-storage#running-your-own">
self-host
</Link>
.
</Prose>
</div>
<ul className="flex flex-col sm:flex-row gap-4 text-sm">
{[
"Data & blob storage",
"Global sync",
"No limits for public alpha",
].map((feature) => (
<li
key={feature}
className="flex items-center gap-1.5 whitespace-nowrap"
>
<span className="text-blue p-1 rounded-full bg-blue-50 dark:text-blue-500 dark:bg-white/10">
<CheckIcon size={12} strokeWidth={3} />
</span>
{feature}
</li>
))}
</ul>
</div>
<Prose size="sm">
Or{" "}
<Link href="/docs/sync-and-storage#running-your-own">
self-host
</Link>
.
</Prose>
</div>
</div>
</div>

View File

@@ -40,8 +40,8 @@ export function ClerkFullLogo(props: SVGProps<SVGSVGElement>) {
}}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M100.405 21.2489C100.421 21.2324 100.442 21.2231 100.465 21.2231C100.493 21.2231 100.518 21.2375 100.533 21.2613L105.275 28.8821C105.321 28.9554 105.401 29 105.487 29L109.75 29C109.946 29 110.066 28.7848 109.963 28.6183L103.457 18.1226C103.399 18.0278 103.41 17.9056 103.485 17.823L109.752 10.908C109.898 10.7473 109.784 10.4901 109.567 10.4901H105.12C105.05 10.4901 104.983 10.5194 104.936 10.5711L97.6842 18.4755C97.5301 18.6435 97.25 18.5345 97.25 18.3065V3.25C97.25 3.11193 97.138 3 97 3H93.25C93.1119 3 93 3.11193 93 3.25V28.75C93 28.8881 93.1119 29 93.25 29L97 29C97.138 29 97.25 28.8881 97.25 28.75V24.7373C97.25 24.6741 97.2739 24.6132 97.317 24.567L100.405 21.2489ZM52.2502 3.25C52.2502 3.11193 52.3621 3 52.5002 3H56.2501C56.3882 3 56.5001 3.11193 56.5001 3.25V28.75C56.5001 28.8881 56.3882 29 56.2501 29H52.5002C52.3621 29 52.2502 28.8881 52.2502 28.75V3.25ZM46.958 23.5912C46.8584 23.5052 46.7094 23.5117 46.6137 23.602C46.0293 24.1537 45.3447 24.595 44.5947 24.9028C43.7719 25.2407 42.8873 25.4108 41.995 25.4028C41.2415 25.4252 40.4913 25.2963 39.7906 25.0241C39.09 24.7519 38.4537 24.3422 37.9209 23.8202C36.9531 22.8322 36.396 21.4215 36.396 19.7399C36.396 16.3735 38.6356 14.0709 41.995 14.0709C42.896 14.0585 43.7888 14.241 44.6094 14.6052C45.3533 14.9355 46.0214 15.4077 46.5748 15.9934C46.6694 16.0936 46.8266 16.1052 46.9309 16.015L49.4625 13.8244C49.5659 13.7349 49.5785 13.5786 49.4873 13.4767C47.583 11.3488 44.5997 10.25 41.7627 10.25C36.0506 10.25 32.0003 14.1031 32.0003 19.7719C32.0003 22.5756 33.0069 24.9365 34.7044 26.6036C36.402 28.2707 38.8203 29.25 41.6108 29.25C45.1097 29.25 47.9259 27.9082 49.577 26.187C49.6739 26.086 49.6632 25.9252 49.5572 25.8338L46.958 23.5912ZM77.1575 20.9877C77.1436 21.1129 77.0371 21.2066 76.9111 21.2066H63.7746C63.615 21.2066 63.4961 21.3546 63.5377 21.5087C64.1913 23.9314 66.1398 25.3973 68.7994 25.3973C69.6959 25.4161 70.5846 25.2317 71.3968 24.8582C72.1536 24.5102 72.8249 24.0068 73.3659 23.3828C73.4314 23.3073 73.5454 23.2961 73.622 23.3602L76.2631 25.6596C76.3641 25.7476 76.3782 25.8999 76.2915 26.0021C74.697 27.8832 72.1135 29.25 68.5683 29.25C63.1142 29.25 59.0001 25.4731 59.0001 19.7348C59.0001 16.9197 59.9693 14.559 61.5847 12.8921C62.4374 12.0349 63.4597 11.3584 64.5882 10.9043C65.7168 10.4502 66.9281 10.2281 68.1473 10.2517C73.6753 10.2517 77.25 14.1394 77.25 19.5075C77.2431 20.0021 77.2123 20.4961 77.1575 20.9877ZM63.6166 17.5038C63.5702 17.6581 63.6894 17.8084 63.8505 17.8084H72.5852C72.7467 17.8084 72.8659 17.6572 72.8211 17.5021C72.2257 15.4416 70.7153 14.0666 68.3696 14.0666C67.6796 14.0447 66.993 14.1696 66.3565 14.4326C65.7203 14.6957 65.149 15.0908 64.6823 15.5907C64.1914 16.1473 63.8285 16.7998 63.6166 17.5038ZM90.2473 10.2527C90.3864 10.2512 90.5 10.3636 90.5 10.5027V14.7013C90.5 14.8469 90.3762 14.9615 90.2311 14.9508C89.8258 14.9207 89.4427 14.8952 89.1916 14.8952C85.9204 14.8952 84 17.1975 84 20.2195V28.75C84 28.8881 83.8881 29 83.75 29H80C79.862 29 79.75 28.8881 79.75 28.75V10.7623C79.75 10.6242 79.862 10.5123 80 10.5123H83.75C83.8881 10.5123 84 10.6242 84 10.7623V13.287C84 13.3013 84.0116 13.3128 84.0258 13.3128C84.034 13.3128 84.0416 13.3089 84.0465 13.3024C85.5124 11.3448 87.676 10.2559 89.9617 10.2559L90.2473 10.2527Z"
className="fill-[#131316] dark:fill-white"
fillOpacity="1"

View File

@@ -1,3 +1,4 @@
import { ThemeToggle } from "@/components/ThemeToggle";
import { socials } from "@/lib/socials";
import { JazzLogo } from "gcmp-design-system/src/app/components/atoms/logos/JazzLogo";
import { Nav } from "gcmp-design-system/src/app/components/organisms/Nav";
@@ -8,6 +9,7 @@ export function JazzNav() {
return (
<Nav
mainLogo={<JazzLogo className="w-24" />}
themeToggle={ThemeToggle}
items={[
{ title: "Jazz Cloud", href: "/cloud" },
{
@@ -26,18 +28,6 @@ export function JazzNav() {
description:
"Get started with using Jazz by learning the core concepts, and going through guides.",
},
{
icon: (
<BoxIcon
className="size-5 stroke-blue dark:stroke-blue-500 shrink-0"
strokeWidth={1.5}
/>
),
title: "API reference",
href: "/api-reference",
description:
"API references for packages like jazz-tools, jazz-react, and more.",
},
{
icon: (
<CodeIcon
@@ -50,6 +40,18 @@ export function JazzNav() {
description:
"Demo and source code for example apps built with Jazz.",
},
{
icon: (
<BoxIcon
className="size-5 stroke-blue dark:stroke-blue-500 shrink-0"
strokeWidth={1.5}
/>
),
title: "API reference",
href: "/api-reference",
description:
"API references for packages like jazz-tools, jazz-react, and more.",
},
],
},
{
@@ -58,13 +60,13 @@ export function JazzNav() {
},
{
title: "Blog",
href: "https://gcmp.io/news",
href: "https://garden.co/news",
firstOnRight: true,
newTab: true,
},
{
title: "Releases",
href: "https://github.com/gardencmp/jazz/releases",
href: "https://github.com/garden-co/jazz/releases",
newTab: true,
},
]}

View File

@@ -0,0 +1,28 @@
export type Example = {
name: string;
slug: string;
description?: string;
illustration?: React.ReactNode;
tech?: string[];
features?: string[];
demoUrl?: string;
showDemo?: boolean;
imageUrl?: string;
codeSamples?: { name: string; content: React.ReactNode }[];
};
export const tech = {
react: "React",
nextjs: "Next.js",
reactNative: "React Native",
vue: "Vue",
};
export const features = {
fileUpload: "File upload",
imageUpload: "Image upload",
passkey: "Passkey auth",
clerk: "Clerk auth",
inviteLink: "Invite link",
coFeed: "CoFeed",
};

View File

@@ -1,5 +1,5 @@
export const socials = {
github: "https://github.com/gardencmp/jazz",
github: "https://github.com/garden-co/jazz",
discord: "https://discord.gg/utDMjHYg42",
x: "https://x.com/jazz_tools",
};

View File

@@ -6,7 +6,7 @@
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=8192 next dev",
"build:generate-docs": "node genDocs.mjs --build",
"build": "pnpm run build:generate-docs && next build",
"build": "pnpm run build:generate-docs && node renderCodeSamples.mjs && next build",
"start": "next start",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write"
@@ -19,18 +19,26 @@
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.5.4",
"@stefanprobst/rehype-extract-toc": "^2.2.0",
"@turf/turf": "^7.1.0",
"@types/mdx": "^2.0.8",
"@types/topojson-client": "^3.1.5",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"clsx": "^2.1.1",
"gcmp-design-system": "workspace:*",
"jazz-browser": "workspace:../*",
"jazz-browser-media-images": "workspace:../*",
"jazz-nodejs": "workspace:../*",
"jazz-react": "workspace:../*",
"jazz-tools": "workspace:../*",
"lucide-react": "^0.436.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-mdx": "^3.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"next": "14.2.15",
"next-themes": "^0.2.1",
"qrcode": "^1.5.4",
"react": "^18",
"react": "18",
"react-dom": "^18",
"rehype-slug": "^6.0.0",
"shiki": "^0.14.6",
@@ -41,13 +49,14 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/geojson": "^7946.0.14",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"postcss": "^8",
"tailwindcss": "^3",
"typedoc": "^0.25.13",
"typescript": "^5.3.3"
"typescript": "5.3.3"
}
}

View File

@@ -0,0 +1,114 @@
import path from "path";
import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
import {
createShikiHighlighter,
renderCodeToHTML,
runTwoSlash,
} from "shiki-twoslash";
const targetDoc = "app/examples/page.tsx";
const targetDocSrc = await readFile(targetDoc, "utf8");
await rm("./codeSamples", { recursive: true, force: true });
[...targetDocSrc.matchAll(/"@\/codeSamples\/(.+?)"/g)].forEach(
async (match) => {
const dir = match[1];
console.log("Rendering", { dir });
const allFiles = Object.fromEntries(
(
await Promise.all(
(
await readdir(path.join("../../", dir))
).map(async (f) => {
if (f.endsWith(".json") || f === "vite-env.d.ts") return undefined;
if (f.endsWith(".ts") || f.endsWith(".tsx")) {
return [f, await readFile(path.join("../../", dir, f), "utf8")];
}
return undefined;
}),
)
).filter((entry) => entry !== undefined),
);
console.log(allFiles);
const components = (
await Promise.all(
Object.entries(allFiles).map(async ([filename, src]) => {
const otherFilesConcat = Object.entries(allFiles)
.filter(([f, _]) => f !== filename)
.map(([f, src]) => `// @filename: ${f}\n// @errors: 2345\n${src}`)
.join("\n\n");
const forFile =
otherFilesConcat +
"\n\n// @filename: " +
filename +
"\n// @errors: 2345" +
"\n// ---cut---\n" +
src.trim();
const highlighter = await createShikiHighlighter({
theme: "css-variables",
});
const twoslash = runTwoSlash(forFile, "ts", {
disableImplicitReactImport: true,
defaultCompilerOptions: {
allowImportingTsExtensions: true,
noEmit: true,
jsx: 4,
strict: true,
paths: {
"jazz-tools": ["../../../packages/jazz-tools"],
"jazz-react": ["../../../packages/jazz-react"],
"hash-slash": ["../../../packages/hash-slash"],
"jazz-browser-media-images": [
"../../../packages/jazz-browser-media-images",
],
},
types: [
"../../../examples/image-upload/node_modules/vite/client",
"../../../examples/reactions/node_modules/vite/client",
],
},
});
const html = renderCodeToHTML(
twoslash.code,
"tsx",
{ twoslash: true },
{
themeName: "css-variables",
},
highlighter,
twoslash,
);
const component = `export function ${
path.basename(filename).slice(0, 1).toUpperCase() +
path.basename(filename).slice(1).replace(".", "_")
}() {\n\treturn <div className="not-prose h-full" dangerouslySetInnerHTML={{__html: \`${html
.replace(/`/g, "\\`")
.replace(/\$/g, "\\$")}\`\n\t}}/>;\n}`;
return component;
}),
)
).join("\n\n");
await mkdir(path.join("./codeSamples/", dir), {
recursive: true,
});
await writeFile(
path.join("./codeSamples/", dir, "index.tsx"),
components,
"utf8",
);
},
);

View File

@@ -0,0 +1,19 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build", "build:generate-docs"],
"outputs": [".next/**", "!.next/cache/**"]
},
"build:generate-docs": {
"inputs": ["../../../packages/*/src/**"],
"outputs": ["app/docs/api/**"],
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["build"]
}
}
}

1689
homepage/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

14
homepage/turbo.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["build"]
}
}
}

View File

@@ -6,7 +6,8 @@
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.8.35"
"cojson": "workspace:*",
"cojson-storage": "workspace:*"
},
"devDependencies": {
"@vitest/browser": "^0.34.1",

View File

@@ -1,39 +1,19 @@
import { CojsonInternalTypes, SessionID } from "cojson";
import { CojsonInternalTypes } from "cojson";
import { SyncPromise } from "./syncPromises";
import RawCoID = CojsonInternalTypes.RawCoID;
import Transaction = CojsonInternalTypes.Transaction;
import Signature = CojsonInternalTypes.Signature;
import {
CoValueRow,
DBClientInterface,
SessionRow,
SignatureAfterRow,
StoredCoValueRow,
StoredSessionRow,
TransactionRow,
} from "cojson-storage";
export type CoValueRow = {
id: CojsonInternalTypes.RawCoID;
header: CojsonInternalTypes.CoValueHeader;
};
export type StoredCoValueRow = CoValueRow & { rowID: number };
export type TransactionRow = {
ses: number;
idx: number;
tx: CojsonInternalTypes.Transaction;
};
export type SignatureAfterRow = {
ses: number;
idx: number;
signature: CojsonInternalTypes.Signature;
};
export type SessionRow = {
coValue: number;
sessionID: SessionID;
lastIdx: number;
lastSignature: CojsonInternalTypes.Signature;
bytesSinceLastSignature?: number;
};
export type StoredSessionRow = SessionRow & { rowID: number };
export class IDBClient {
export class IDBClient implements DBClientInterface {
private db;
currentTx:
@@ -148,29 +128,29 @@ export class IDBClient {
}
async getNewTransactionInSession(
sessionRow: StoredSessionRow,
sessionRowId: number,
firstNewTxIdx: number,
): Promise<TransactionRow[]> {
return this.makeRequest<TransactionRow[]>(({ transactions }) =>
transactions.getAll(
IDBKeyRange.bound(
[sessionRow.rowID, firstNewTxIdx],
[sessionRow.rowID, Infinity],
[sessionRowId, firstNewTxIdx],
[sessionRowId, Infinity],
),
),
);
}
async getSignatures(
sessionRow: StoredSessionRow,
sessionRowId: number,
firstNewTxIdx: number,
): Promise<SignatureAfterRow[]> {
return this.makeRequest<SignatureAfterRow[]>(
({ signatureAfter }: { signatureAfter: IDBObjectStore }) =>
signatureAfter.getAll(
IDBKeyRange.bound(
[sessionRow.rowID, firstNewTxIdx],
[sessionRow.rowID, Infinity],
[sessionRowId, firstNewTxIdx],
[sessionRowId, Infinity],
),
),
);
@@ -191,10 +171,13 @@ export class IDBClient {
)) as number;
}
async addSessionUpdate(
sessionRow: StoredSessionRow | undefined,
sessionUpdate: SessionRow,
): Promise<number> {
async addSessionUpdate({
sessionUpdate,
sessionRow,
}: {
sessionUpdate: SessionRow;
sessionRow?: StoredSessionRow;
}): Promise<number> {
return this.makeRequest<number>(({ sessions }) =>
sessions.put(
sessionRow?.rowID
@@ -221,7 +204,7 @@ export class IDBClient {
);
}
addSignatureAfter({
async addSignatureAfter({
sessionRowID,
idx,
signature,
@@ -234,4 +217,8 @@ export class IDBClient {
} satisfies SignatureAfterRow),
);
}
async unitOfWork(operationsCallback: () => unknown[]) {
return Promise.all(operationsCallback());
}
}

View File

@@ -4,12 +4,12 @@ import {
Peer,
cojsonInternals,
} from "cojson";
import { IDBClient } from "./idbClient";
import { SyncManager } from "./syncManager";
import { SyncManager } from "cojson-storage";
import { IDBClient } from "./idbClient.js";
export class IDBNode {
private dbClient: IDBClient;
private syncManager: SyncManager;
private readonly dbClient: IDBClient;
private readonly syncManager: SyncManager;
constructor(
db: IDBDatabase,

View File

@@ -1 +1 @@
export { IDBNode, IDBNode as IDBStorage } from "./idbNode";
export { IDBNode, IDBNode as IDBStorage } from "./idbNode.js";

View File

@@ -8,9 +8,10 @@
"dependencies": {
"better-sqlite3": "^8.5.2",
"cojson": "workspace:0.8.35",
"typescript": "^5.3.3"
"cojson-storage": "workspace:*"
},
"devDependencies": {
"typescript": "^5.3.3",
"@types/better-sqlite3": "^7.6.4"
},
"scripts": {

View File

@@ -1,609 +1 @@
import {
CojsonInternalTypes,
IncomingSyncStream,
MAX_RECOMMENDED_TX_SIZE,
OutgoingSyncQueue,
Peer,
RawAccountID,
SessionID,
SyncMessage,
cojsonInternals,
} from "cojson";
import Database, { Database as DatabaseT } from "better-sqlite3";
type CoValueRow = {
id: CojsonInternalTypes.RawCoID;
header: string;
};
type StoredCoValueRow = CoValueRow & { rowID: number };
type SessionRow = {
coValue: number;
sessionID: SessionID;
lastIdx: number;
lastSignature: CojsonInternalTypes.Signature;
bytesSinceLastSignature?: number;
};
type StoredSessionRow = SessionRow & { rowID: number };
type TransactionRow = {
ses: number;
idx: number;
tx: string;
};
type SignatureAfterRow = {
ses: number;
idx: number;
signature: CojsonInternalTypes.Signature;
};
export class SQLiteStorage {
toLocalNode: OutgoingSyncQueue;
db: DatabaseT;
constructor(
db: DatabaseT,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
this.db = db;
this.toLocalNode = toLocalNode;
const processMessages = async () => {
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
await this.handleSyncMessage(msg);
} catch (e) {
console.error(
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
)}`,
{ cause: e },
),
);
}
}
};
processMessages().catch((e) =>
console.error("Error in processMessages in sqlite", e),
);
}
static async asPeer({
filename,
trace,
localNodeName = "local",
}: {
filename: string;
trace?: boolean;
localNodeName?: string;
}): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "storage", trace, crashOnClose: true },
);
await SQLiteStorage.open(
filename,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
);
return { ...storageAsPeer, priority: 100 };
}
static async open(
filename: string,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
const db = Database(filename);
db.pragma("journal_mode = WAL");
const oldVersion = (
db.pragma("user_version") as [{ user_version: number }]
)[0].user_version as number;
console.log("DB version", oldVersion);
if (oldVersion === 0) {
console.log("Migration 0 -> 1: Basic schema");
db.prepare(
`CREATE TABLE IF NOT EXISTS transactions (
ses INTEGER,
idx INTEGER,
tx TEXT NOT NULL,
PRIMARY KEY (ses, idx)
) WITHOUT ROWID;`,
).run();
db.prepare(
`CREATE TABLE IF NOT EXISTS sessions (
rowID INTEGER PRIMARY KEY,
coValue INTEGER NOT NULL,
sessionID TEXT NOT NULL,
lastIdx INTEGER,
lastSignature TEXT,
UNIQUE (sessionID, coValue)
);`,
).run();
db.prepare(
`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`,
).run();
db.prepare(
`CREATE TABLE IF NOT EXISTS coValues (
rowID INTEGER PRIMARY KEY,
id TEXT NOT NULL UNIQUE,
header TEXT NOT NULL UNIQUE
);`,
).run();
db.prepare(
`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`,
).run();
db.pragma("user_version = 1");
console.log("Migration 0 -> 1: Basic schema - done");
}
if (oldVersion <= 1) {
// fix embarrassing off-by-one error for transaction indices
console.log(
"Migration 1 -> 2: Fix off-by-one error for transaction indices",
);
const txs = db
.prepare(`SELECT * FROM transactions`)
.all() as TransactionRow[];
for (const tx of txs) {
db.prepare(`DELETE FROM transactions WHERE ses = ? AND idx = ?`).run(
tx.ses,
tx.idx,
);
tx.idx -= 1;
db.prepare(
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
).run(tx.ses, tx.idx, tx.tx);
}
db.pragma("user_version = 2");
console.log(
"Migration 1 -> 2: Fix off-by-one error for transaction indices - done",
);
}
if (oldVersion <= 2) {
console.log("Migration 2 -> 3: Add signatureAfter");
db.prepare(
`CREATE TABLE IF NOT EXISTS signatureAfter (
ses INTEGER,
idx INTEGER,
signature TEXT NOT NULL,
PRIMARY KEY (ses, idx)
) WITHOUT ROWID;`,
).run();
db.prepare(
`ALTER TABLE sessions ADD COLUMN bytesSinceLastSignature INTEGER;`,
).run();
db.pragma("user_version = 3");
console.log("Migration 2 -> 3: Add signatureAfter - done");
}
return new SQLiteStorage(db, fromLocalNode, toLocalNode);
}
async handleSyncMessage(msg: SyncMessage) {
switch (msg.action) {
case "load":
await this.handleLoad(msg);
break;
case "content":
await this.handleContent(msg);
break;
case "known":
await this.handleKnown(msg);
break;
case "done":
await this.handleDone(msg);
break;
}
}
async sendNewContentAfter(
theirKnown: CojsonInternalTypes.CoValueKnownState,
asDependencyOf?: CojsonInternalTypes.RawCoID,
) {
const coValueRow = (await this.db
.prepare(`SELECT * FROM coValues WHERE id = ?`)
.get(theirKnown.id)) as StoredCoValueRow | undefined;
const allOurSessions = coValueRow
? (this.db
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
.all(coValueRow.rowID) as StoredSessionRow[])
: [];
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
id: theirKnown.id,
header: !!coValueRow,
sessions: {},
};
let parsedHeader;
try {
parsedHeader = (coValueRow?.header && JSON.parse(coValueRow.header)) as
| CojsonInternalTypes.CoValueHeader
| undefined;
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in header",
e,
coValueRow?.header,
);
return;
}
const priority = cojsonInternals.getPriorityFromHeader(parsedHeader);
const newContentPieces: CojsonInternalTypes.NewContentMessage[] = [
{
action: "content",
id: theirKnown.id,
header: theirKnown.header ? undefined : parsedHeader,
new: {},
priority,
},
];
for (const sessionRow of allOurSessions) {
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
if (
sessionRow.lastIdx > (theirKnown.sessions[sessionRow.sessionID] || 0)
) {
const firstNewTxIdx = theirKnown.sessions[sessionRow.sessionID] || 0;
const signaturesAndIdxs = this.db
.prepare<[number, number]>(
`SELECT * FROM signatureAfter WHERE ses = ? AND idx >= ?`,
)
.all(sessionRow.rowID, firstNewTxIdx) as SignatureAfterRow[];
// console.log(
// theirKnown.id,
// "signaturesAndIdxs",
// JSON.stringify(signaturesAndIdxs)
// );
const newTxInSession = this.db
.prepare<[number, number]>(
`SELECT * FROM transactions WHERE ses = ? AND idx >= ?`,
)
.all(sessionRow.rowID, firstNewTxIdx) as TransactionRow[];
let idx = firstNewTxIdx;
// console.log(
// theirKnown.id,
// "newTxInSession",
// newTxInSession.length
// );
for (const tx of newTxInSession) {
let sessionEntry =
newContentPieces[newContentPieces.length - 1]!.new[
sessionRow.sessionID
];
if (!sessionEntry) {
sessionEntry = {
after: idx,
lastSignature:
"WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
newTransactions: [],
};
newContentPieces[newContentPieces.length - 1]!.new[
sessionRow.sessionID
] = sessionEntry;
}
let parsedTx;
try {
parsedTx = JSON.parse(tx.tx);
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in transaction",
e,
tx.tx,
);
break;
}
sessionEntry.newTransactions.push(parsedTx);
if (signaturesAndIdxs[0] && idx === signaturesAndIdxs[0].idx) {
sessionEntry.lastSignature = signaturesAndIdxs[0].signature;
signaturesAndIdxs.shift();
newContentPieces.push({
action: "content",
id: theirKnown.id,
new: {},
priority,
});
} else if (idx === firstNewTxIdx + newTxInSession.length - 1) {
sessionEntry.lastSignature = sessionRow.lastSignature;
}
idx += 1;
}
}
}
const dependedOnCoValues =
parsedHeader?.ruleset.type === "group"
? newContentPieces
.flatMap((piece) => Object.values(piece.new))
.flatMap((sessionEntry) =>
sessionEntry.newTransactions.flatMap((tx) => {
if (tx.privacy !== "trusting") return [];
// TODO: avoid parsing here?
let parsedChanges;
try {
parsedChanges = cojsonInternals.parseJSON(tx.changes);
} catch (e) {
console.warn(
theirKnown.id,
"Invalid JSON in transaction",
e,
tx.changes,
);
return [];
}
return cojsonInternals.getGroupDependentKeyList(
parsedChanges.map(
(change) =>
change &&
typeof change === "object" &&
"op" in change &&
change.op === "set" &&
"key" in change &&
change.key,
),
);
}),
)
: parsedHeader?.ruleset.type === "ownedByGroup"
? [
parsedHeader?.ruleset.group,
...new Set(
newContentPieces.flatMap((piece) =>
Object.keys(piece.new)
.map((sessionID) =>
cojsonInternals.accountOrAgentIDfromSessionID(
sessionID as SessionID,
),
)
.filter(
(accountID): accountID is RawAccountID =>
cojsonInternals.isAccountID(accountID) &&
accountID !== theirKnown.id,
),
),
),
]
: [];
for (const dependedOnCoValue of dependedOnCoValues) {
await this.sendNewContentAfter(
{ id: dependedOnCoValue, header: false, sessions: {} },
asDependencyOf || theirKnown.id,
);
}
this.toLocalNode
.push({
action: "known",
...ourKnown,
asDependencyOf,
})
.catch((e) => console.error("Error while pushing known", e));
const nonEmptyNewContentPieces = newContentPieces.filter(
(piece) => piece.header || Object.keys(piece.new).length > 0,
);
// console.log(theirKnown.id, nonEmptyNewContentPieces);
for (const piece of nonEmptyNewContentPieces) {
this.toLocalNode
.push(piece)
.catch((e) => console.error("Error while pushing content piece", e));
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
return this.sendNewContentAfter(msg);
}
async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
let storedCoValueRowID = (
this.db
.prepare<CojsonInternalTypes.RawCoID>(
`SELECT rowID FROM coValues WHERE id = ?`,
)
.get(msg.id) as StoredCoValueRow | undefined
)?.rowID;
if (storedCoValueRowID === undefined) {
const header = msg.header;
if (!header) {
console.error("Expected to be sent header first");
this.toLocalNode
.push({
action: "known",
id: msg.id,
header: false,
sessions: {},
isCorrection: true,
})
.catch((e) => console.error("Error while pushing known", e));
return;
}
storedCoValueRowID = this.db
.prepare<[CojsonInternalTypes.RawCoID, string]>(
`INSERT INTO coValues (id, header) VALUES (?, ?)`,
)
.run(msg.id, JSON.stringify(header)).lastInsertRowid as number;
}
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
id: msg.id,
header: true,
sessions: {},
};
let invalidAssumptions = false;
this.db.transaction(() => {
const allOurSessions = (
this.db
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
.all(storedCoValueRowID!) as StoredSessionRow[]
).reduce(
(acc, row) => {
acc[row.sessionID] = row;
return acc;
},
{} as { [sessionID: string]: StoredSessionRow },
);
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
const sessionRow = allOurSessions[sessionID];
if (sessionRow) {
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
}
if ((sessionRow?.lastIdx || 0) < (msg.new[sessionID]?.after || 0)) {
invalidAssumptions = true;
} else {
const newTransactions = msg.new[sessionID]?.newTransactions || [];
const actuallyNewOffset =
(sessionRow?.lastIdx || 0) - (msg.new[sessionID]?.after || 0);
const actuallyNewTransactions =
newTransactions.slice(actuallyNewOffset);
let newBytesSinceLastSignature =
(sessionRow?.bytesSinceLastSignature || 0) +
actuallyNewTransactions.reduce(
(sum, tx) =>
sum +
(tx.privacy === "private"
? tx.encryptedChanges.length
: tx.changes.length),
0,
);
const newLastIdx =
(sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
let shouldWriteSignature = false;
if (newBytesSinceLastSignature > MAX_RECOMMENDED_TX_SIZE) {
shouldWriteSignature = true;
newBytesSinceLastSignature = 0;
}
let nextIdx = sessionRow?.lastIdx || 0;
const sessionUpdate = {
coValue: storedCoValueRowID!,
sessionID: sessionID,
lastIdx: newLastIdx,
lastSignature: msg.new[sessionID]!.lastSignature,
bytesSinceLastSignature: newBytesSinceLastSignature,
};
const upsertedSession = this.db
.prepare<[number, string, number, string, number]>(
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
RETURNING rowID`,
)
.get(
sessionUpdate.coValue,
sessionUpdate.sessionID,
sessionUpdate.lastIdx,
sessionUpdate.lastSignature,
sessionUpdate.bytesSinceLastSignature,
) as { rowID: number };
const sessionRowID = upsertedSession.rowID;
if (shouldWriteSignature) {
this.db
.prepare<[number, number, string]>(
`INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)`,
)
.run(
sessionRowID,
// TODO: newLastIdx is a misnomer, it's actually more like nextIdx or length
newLastIdx - 1,
msg.new[sessionID]!.lastSignature,
);
}
for (const newTransaction of actuallyNewTransactions) {
this.db
.prepare<[number, number, string]>(
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
)
.run(sessionRowID, nextIdx, JSON.stringify(newTransaction));
nextIdx++;
}
}
}
})();
if (invalidAssumptions) {
this.toLocalNode
.push({
action: "known",
...ourKnown,
isCorrection: invalidAssumptions,
})
.catch((e) => console.error("Error while pushing known", e));
}
}
handleKnown(msg: CojsonInternalTypes.KnownStateMessage) {
return this.sendNewContentAfter(msg);
}
handleDone(_msg: CojsonInternalTypes.DoneMessage) {}
}
export { SQLiteNode, SQLiteNode as SQLiteStorage } from "./sqliteNode.js";

View File

@@ -0,0 +1,153 @@
import { Database as DatabaseT } from "better-sqlite3";
import { CojsonInternalTypes, OutgoingSyncQueue, SessionID } from "cojson";
import RawCoID = CojsonInternalTypes.RawCoID;
import Signature = CojsonInternalTypes.Signature;
import Transaction = CojsonInternalTypes.Transaction;
import {
DBClientInterface,
SessionRow,
SignatureAfterRow,
StoredCoValueRow,
StoredSessionRow,
TransactionRow,
} from "cojson-storage";
export type RawCoValueRow = {
id: CojsonInternalTypes.RawCoID;
header: string;
};
export type RawTransactionRow = {
ses: number;
idx: number;
tx: string;
};
export class SQLiteClient implements DBClientInterface {
private readonly db: DatabaseT;
private readonly toLocalNode: OutgoingSyncQueue;
constructor(db: DatabaseT, toLocalNode: OutgoingSyncQueue) {
this.db = db;
this.toLocalNode = toLocalNode;
}
getCoValue(coValueId: RawCoID): StoredCoValueRow | undefined {
const coValueRow = this.db
.prepare(`SELECT * FROM coValues WHERE id = ?`)
.get(coValueId) as RawCoValueRow & { rowID: number };
if (!coValueRow) return;
try {
const parsedHeader = (coValueRow?.header &&
JSON.parse(coValueRow.header)) as CojsonInternalTypes.CoValueHeader;
return {
...coValueRow,
header: parsedHeader,
};
} catch (e) {
console.warn(coValueId, "Invalid JSON in header", e, coValueRow?.header);
return;
}
}
getCoValueSessions(coValueRowId: number): StoredSessionRow[] {
return this.db
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
.all(coValueRowId) as StoredSessionRow[];
}
getNewTransactionInSession(
sessionRowId: number,
firstNewTxIdx: number,
): TransactionRow[] {
const txs = this.db
.prepare<[number, number]>(
`SELECT * FROM transactions WHERE ses = ? AND idx >= ?`,
)
.all(sessionRowId, firstNewTxIdx) as RawTransactionRow[];
try {
return txs.map((transactionRow) => ({
...transactionRow,
tx: JSON.parse(transactionRow.tx) as Transaction,
}));
} catch (e) {
console.warn("Invalid JSON in transaction", e);
return [];
}
}
getSignatures(
sessionRowId: number,
firstNewTxIdx: number,
): SignatureAfterRow[] {
return this.db
.prepare<[number, number]>(
`SELECT * FROM signatureAfter WHERE ses = ? AND idx >= ?`,
)
.all(sessionRowId, firstNewTxIdx) as SignatureAfterRow[];
}
addCoValue(msg: CojsonInternalTypes.NewContentMessage): number {
return this.db
.prepare<[CojsonInternalTypes.RawCoID, string]>(
`INSERT INTO coValues (id, header) VALUES (?, ?)`,
)
.run(msg.id, JSON.stringify(msg.header)).lastInsertRowid as number;
}
addSessionUpdate({
sessionUpdate,
sessionRow,
}: {
sessionUpdate: SessionRow;
sessionRow?: StoredSessionRow;
}): number {
return (
this.db
.prepare<[number, string, number, string, number | undefined]>(
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
RETURNING rowID`,
)
.get(
sessionUpdate.coValue,
sessionUpdate.sessionID,
sessionUpdate.lastIdx,
sessionUpdate.lastSignature,
sessionUpdate.bytesSinceLastSignature,
) as { rowID: number }
).rowID;
}
addTransaction(
sessionRowID: number,
nextIdx: number,
newTransaction: Transaction,
) {
this.db
.prepare<[number, number, string]>(
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
)
.run(sessionRowID, nextIdx, JSON.stringify(newTransaction));
}
addSignatureAfter({
sessionRowID,
idx,
signature,
}: { sessionRowID: number; idx: number; signature: Signature }) {
this.db
.prepare<[number, number, string]>(
`INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)`,
)
.run(sessionRowID, idx, signature);
}
unitOfWork(operationsCallback: () => any[]) {
this.db.transaction(operationsCallback)();
}
}

View File

@@ -0,0 +1,181 @@
import Database, { Database as DatabaseT } from "better-sqlite3";
import {
IncomingSyncStream,
OutgoingSyncQueue,
Peer,
cojsonInternals,
} from "cojson";
import { SyncManager, TransactionRow } from "cojson-storage";
import { SQLiteClient } from "./sqliteClient.js";
export class SQLiteNode {
private readonly syncManager: SyncManager;
private readonly dbClient: SQLiteClient;
constructor(
db: DatabaseT,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
this.dbClient = new SQLiteClient(db, toLocalNode);
this.syncManager = new SyncManager(this.dbClient, toLocalNode);
const processMessages = async () => {
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
await this.syncManager.handleSyncMessage(msg);
} catch (e) {
console.error(
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
)}`,
{ cause: e },
),
);
}
}
};
processMessages().catch((e) =>
console.error("Error in processMessages in sqlite", e),
);
}
static async asPeer({
filename,
trace,
localNodeName = "local",
}: {
filename: string;
trace?: boolean;
localNodeName?: string;
}): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "storage", trace, crashOnClose: true },
);
await SQLiteNode.open(
filename,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
);
return { ...storageAsPeer, priority: 100 };
}
static async open(
filename: string,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
const db = Database(filename);
db.pragma("journal_mode = WAL");
const oldVersion = (
db.pragma("user_version") as [{ user_version: number }]
)[0].user_version as number;
console.log("DB version", oldVersion);
if (oldVersion === 0) {
console.log("Migration 0 -> 1: Basic schema");
db.prepare(
`CREATE TABLE IF NOT EXISTS transactions (
ses INTEGER,
idx INTEGER,
tx TEXT NOT NULL,
PRIMARY KEY (ses, idx)
) WITHOUT ROWID;`,
).run();
db.prepare(
`CREATE TABLE IF NOT EXISTS sessions (
rowID INTEGER PRIMARY KEY,
coValue INTEGER NOT NULL,
sessionID TEXT NOT NULL,
lastIdx INTEGER,
lastSignature TEXT,
UNIQUE (sessionID, coValue)
);`,
).run();
db.prepare(
`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`,
).run();
db.prepare(
`CREATE TABLE IF NOT EXISTS coValues (
rowID INTEGER PRIMARY KEY,
id TEXT NOT NULL UNIQUE,
header TEXT NOT NULL UNIQUE
);`,
).run();
db.prepare(
`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`,
).run();
db.pragma("user_version = 1");
console.log("Migration 0 -> 1: Basic schema - done");
}
if (oldVersion <= 1) {
// fix embarrassing off-by-one error for transaction indices
console.log(
"Migration 1 -> 2: Fix off-by-one error for transaction indices",
);
const txs = db
.prepare(`SELECT * FROM transactions`)
.all() as TransactionRow[];
for (const tx of txs) {
db.prepare(`DELETE FROM transactions WHERE ses = ? AND idx = ?`).run(
tx.ses,
tx.idx,
);
tx.idx -= 1;
db.prepare(
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
).run(tx.ses, tx.idx, tx.tx);
}
db.pragma("user_version = 2");
console.log(
"Migration 1 -> 2: Fix off-by-one error for transaction indices - done",
);
}
if (oldVersion <= 2) {
console.log("Migration 2 -> 3: Add signatureAfter");
db.prepare(
`CREATE TABLE IF NOT EXISTS signatureAfter (
ses INTEGER,
idx INTEGER,
signature TEXT NOT NULL,
PRIMARY KEY (ses, idx)
) WITHOUT ROWID;`,
).run();
db.prepare(
`ALTER TABLE sessions ADD COLUMN bytesSinceLastSignature INTEGER;`,
).run();
db.pragma("user_version = 3");
console.log("Migration 2 -> 3: Add signatureAfter - done!!");
}
return new SQLiteNode(db, fromLocalNode, toLocalNode);
}
}

171
packages/cojson-storage/.gitignore vendored Normal file
View File

@@ -0,0 +1,171 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -0,0 +1,2 @@
coverage
node_modules

View File

@@ -0,0 +1,19 @@
Copyright 2024, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,3 @@
# CoJSON Storage IndexedDB
This implements persistence sync service for CoJSON / Jazz (see [jazz.tools](https://jazz.tools)).

View File

@@ -0,0 +1,24 @@
{
"name": "cojson-storage",
"version": "0.8.35",
"main": "dist/index.js",
"type": "module",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:*"
},
"devDependencies": {
"vitest": "1.5.3",
"typescript": "^5.3.3"
},
"scripts": {
"dev": "tsc --watch --sourceMap --outDir dist",
"test": "vitest --run --root ../../ --project cojson-storage",
"test:watch": "vitest --watch --root ../../ --project cojson-storage",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist",
"prepublishOnly": "npm run build"
}
}

View File

@@ -0,0 +1,2 @@
export * from "./types.js";
export { SyncManager } from "./syncManager.js";

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