Compare commits
73 Commits
jazz-nodej
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d14509e22f | ||
|
|
f0c8340078 | ||
|
|
a62aa97774 | ||
|
|
801a886efb | ||
|
|
3354c0a0d8 | ||
|
|
76753454c2 | ||
|
|
9809666427 | ||
|
|
5fab04a8f4 | ||
|
|
a3d3326f4b | ||
|
|
3b22dada26 | ||
|
|
9dd9366734 | ||
|
|
48dd00f453 | ||
|
|
cc361aefe5 | ||
|
|
9e4438cb54 | ||
|
|
08706d557a | ||
|
|
02c1ec63cc | ||
|
|
df797dedcd | ||
|
|
74cb08e7d4 | ||
|
|
73720a8cc4 | ||
|
|
50c77fa788 | ||
|
|
9977a4ee85 | ||
|
|
441fe27802 | ||
|
|
1afbd2c7cc | ||
|
|
4955e39af5 | ||
|
|
ace151696c | ||
|
|
db9560ebc5 | ||
|
|
80b572710e | ||
|
|
bbb9d45969 | ||
|
|
83e9a3eaa8 | ||
|
|
9ff7e68f7d | ||
|
|
06740e840a | ||
|
|
b0e2c4fd4b | ||
|
|
4c1922c10e | ||
|
|
76bad9b5c3 | ||
|
|
7f16f2705e | ||
|
|
d12594e521 | ||
|
|
cf96350b01 | ||
|
|
947030433f | ||
|
|
b4cebd732e | ||
|
|
d0d95e6d5d | ||
|
|
a75383ac6a | ||
|
|
de503b6120 | ||
|
|
f7ae41254f | ||
|
|
8f348b28c6 | ||
|
|
3b185b4cd3 | ||
|
|
df5dc513bf | ||
|
|
8e6783ad88 | ||
|
|
79bf6f478f | ||
|
|
7fd93a5a61 | ||
|
|
8f9687323f | ||
|
|
47ee25786f | ||
|
|
0009aa19b2 | ||
|
|
741b9cbada | ||
|
|
259ade3099 | ||
|
|
b6f2da2221 | ||
|
|
44157945a0 | ||
|
|
4ff7bb500a | ||
|
|
db5ea54338 | ||
|
|
7c7880a9b2 | ||
|
|
49082a5aad | ||
|
|
b6653555f5 | ||
|
|
c9fd16ce21 | ||
|
|
e60f34d9e6 | ||
|
|
a04c7dca7a | ||
|
|
e067c29d81 | ||
|
|
1357306d1b | ||
|
|
3c6d9b20c1 | ||
|
|
f597316267 | ||
|
|
17f8bc25c3 | ||
|
|
c1d652cf7f | ||
|
|
10f3e4aabd | ||
|
|
63f5574003 | ||
|
|
9bb5c4ca5f |
6
.changeset/heavy-beds-whisper.md
Normal file
6
.changeset/heavy-beds-whisper.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"cojson": patch
|
||||
"jazz-tools": patch
|
||||
---
|
||||
|
||||
Optimise large record-like CoMaps for access of latest value
|
||||
7
.changeset/many-rabbits-obey.md
Normal file
7
.changeset/many-rabbits-obey.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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**:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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/
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
@@ -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/
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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/
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/
|
||||
@@ -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/>`
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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/
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
16
homepage/gcmp/components/ThemeProvider.tsx
Normal file
16
homepage/gcmp/components/ThemeProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
homepage/gcmp/components/ThemeToggle.tsx
Normal file
10
homepage/gcmp/components/ThemeToggle.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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>
|
||||
@@ -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).
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
16
homepage/homepage/components/ThemeProvider.tsx
Normal file
16
homepage/homepage/components/ThemeProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
homepage/homepage/components/ThemeToggle.tsx
Normal file
10
homepage/homepage/components/ThemeToggle.tsx
Normal 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} />;
|
||||
}
|
||||
226
homepage/homepage/components/cloud/latencyMap.tsx
Normal file
226
homepage/homepage/components/cloud/latencyMap.tsx
Normal 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">
|
||||
<{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"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
52
homepage/homepage/components/cloud/mapTooltip.tsx
Normal file
52
homepage/homepage/components/cloud/mapTooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6019
homepage/homepage/components/cloud/ne_110m_land.json
Normal file
6019
homepage/homepage/components/cloud/ne_110m_land.json
Normal file
File diff suppressed because it is too large
Load Diff
14790
homepage/homepage/components/cloud/pings.json
Normal file
14790
homepage/homepage/components/cloud/pings.json
Normal file
File diff suppressed because it is too large
Load Diff
41
homepage/homepage/components/cloud/usePingColorThresholds.ts
Normal file
41
homepage/homepage/components/cloud/usePingColorThresholds.ts
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
33
homepage/homepage/components/examples/ExampleCard.tsx
Normal file
33
homepage/homepage/components/examples/ExampleCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
homepage/homepage/components/examples/ExampleDemo.tsx
Normal file
41
homepage/homepage/components/examples/ExampleDemo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
homepage/homepage/components/examples/ExampleLinks.tsx
Normal file
21
homepage/homepage/components/examples/ExampleLinks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
homepage/homepage/components/examples/ExampleTags.tsx
Normal file
26
homepage/homepage/components/examples/ExampleTags.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
CodeExampleTabs as CodeExampleTabsClient,
|
||||
CodeExampleTabsProps,
|
||||
} from "@/components/CodeExampleTabs";
|
||||
} from "@/components/examples/CodeExampleTabs";
|
||||
|
||||
import {
|
||||
ContentByFramework as ContentByFrameworkClient,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
|
||||
28
homepage/homepage/lib/example.ts
Normal file
28
homepage/homepage/lib/example.ts
Normal 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",
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
114
homepage/homepage/renderCodeSamples.mjs
Normal file
114
homepage/homepage/renderCodeSamples.mjs
Normal 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",
|
||||
);
|
||||
},
|
||||
);
|
||||
19
homepage/homepage/turbo.json
Normal file
19
homepage/homepage/turbo.json
Normal 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
1689
homepage/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
14
homepage/turbo.json
Normal file
14
homepage/turbo.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { IDBNode, IDBNode as IDBStorage } from "./idbNode";
|
||||
export { IDBNode, IDBNode as IDBStorage } from "./idbNode.js";
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
153
packages/cojson-storage-sqlite/src/sqliteClient.ts
Normal file
153
packages/cojson-storage-sqlite/src/sqliteClient.ts
Normal 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)();
|
||||
}
|
||||
}
|
||||
181
packages/cojson-storage-sqlite/src/sqliteNode.ts
Normal file
181
packages/cojson-storage-sqlite/src/sqliteNode.ts
Normal 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
171
packages/cojson-storage/.gitignore
vendored
Normal 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
|
||||
2
packages/cojson-storage/.npmignore
Normal file
2
packages/cojson-storage/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
19
packages/cojson-storage/LICENSE.txt
Normal file
19
packages/cojson-storage/LICENSE.txt
Normal 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.
|
||||
3
packages/cojson-storage/README.md
Normal file
3
packages/cojson-storage/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# CoJSON Storage IndexedDB
|
||||
|
||||
This implements persistence sync service for CoJSON / Jazz (see [jazz.tools](https://jazz.tools)).
|
||||
24
packages/cojson-storage/package.json
Normal file
24
packages/cojson-storage/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
2
packages/cojson-storage/src/index.ts
Normal file
2
packages/cojson-storage/src/index.ts
Normal 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
Reference in New Issue
Block a user