Compare commits

..

34 Commits

Author SHA1 Message Date
Guido D'Orsi
9356ffbd4e Merge pull request #1975 from garden-co/changeset-release/main
Version Packages
2025-04-23 10:17:41 +02:00
github-actions[bot]
328227316c Version Packages 2025-04-23 08:06:44 +00:00
Guido D'Orsi
dd5fe12dfe Merge pull request #1972 from garden-co/fix/fatal-error-coValue-creation
fix: do not crash when loading a CoValue core without the group in the sync server
2025-04-23 10:04:38 +02:00
Guido D'Orsi
f837cfe994 fix: do not crash when loading a CoValue core without the group in the sync server 2025-04-23 01:48:38 +02:00
Guido D'Orsi
978c30a7ca Merge pull request #1971 from garden-co/biome-ignore-package-json
fix(biome): ignore package.json files
2025-04-22 22:37:24 +02:00
Guido D'Orsi
e2c02824e7 fix(biome): ignore package.json files 2025-04-22 22:36:09 +02:00
Guido D'Orsi
6dac7c7ce5 Merge pull request #1968 from garden-co/changeset-release/main
Version Packages
2025-04-22 20:09:29 +02:00
github-actions[bot]
d0724a2c13 Version Packages 2025-04-22 18:04:57 +00:00
Guido D'Orsi
4c632e14a4 Merge pull request #1967 from garden-co/fix/colist-null-values
fix(coList): handle null values when using $each: true
2025-04-22 20:02:58 +02:00
Guido D'Orsi
d7238267d9 fix(passkey-svelte): add jazz-tools as dependency 2025-04-22 20:00:59 +02:00
Guido D'Orsi
a6cf01f867 fix(coList): handle null values when using : true 2025-04-22 19:52:34 +02:00
Guido D'Orsi
17d148b1d2 Merge pull request #1803 from garden-co/jazz-paper-scissors
feat: Jazz paper scissors example
2025-04-22 18:44:52 +02:00
Guido D'Orsi
2544782988 feat(dx): simplify dev setup using npm-run-all 2025-04-22 18:44:12 +02:00
Margaret Culotta
8f42c7a749 merge main, handle conflict in pnpm-lock 2025-04-22 18:40:31 +02:00
Margaret Culotta
49784a7e65 remove additional icons from manifest.json 2025-04-18 08:44:57 -05:00
Guido D'Orsi
935cd745ba chore: align deps 2025-04-18 13:46:47 +02:00
Guido D'Orsi
608d06d46f chore: throw an error when the .env is missing 2025-04-18 13:43:26 +02:00
Guido D'Orsi
7690e19014 docs: update readme 2025-04-18 13:41:22 +02:00
Guido D'Orsi
4626c79c46 chore: clean lockfile 2025-04-18 13:34:48 +02:00
Guido D'Orsi
aad0bd60f4 fix: remove postinstall 2025-04-18 13:29:34 +02:00
Guido D'Orsi
1fbe8d9651 Merge remote-tracking branch 'origin/main' into jazz-paper-scissors 2025-04-18 13:25:45 +02:00
Margaret Culotta
6e28ac946b update readme 2025-04-15 21:19:22 -05:00
Margaret Culotta
cfce22fc63 Merge branch 'main' into jazz-paper-scissors 2025-04-15 15:40:14 -05:00
Guido D'Orsi
ecfc883419 feat: fix build and add new game button 2025-04-11 13:57:39 +02:00
Guido D'Orsi
4aded04223 Merge remote-tracking branch 'origin/main' into jazz-paper-scissors 2025-04-11 12:18:27 +02:00
Guido D'Orsi
3167ff16cd Merge remote-tracking branch 'origin/main' into jazz-paper-scissors 2025-04-07 11:22:52 +02:00
Margaret Culotta
2e70b2b295 remove unnecessary file, adjust prettierignore 2025-04-04 13:41:20 -05:00
Margaret Culotta
9a5e6eed16 clean up console.logs and unused types 2025-04-04 11:48:11 -05:00
Guido D'Orsi
28d74d73f8 fix: add resolve 2025-04-04 16:40:38 +02:00
Margaret Culotta
6c9d90449a clean up styles, simplify 2025-04-03 22:05:43 -05:00
Margaret Culotta
5398978d74 game play is working. Timing issue with waiting room adding game? 2025-04-03 16:15:35 -05:00
Margaret Culotta
53148db482 clean up 2025-04-03 11:28:12 -05:00
Margaret Culotta
44a0b54026 add game page for first and second player 2025-04-03 08:30:45 -05:00
Giordano Ricci
c877d377d2 first commit 2025-03-31 15:40:04 +01:00
131 changed files with 4814 additions and 335 deletions

View File

@@ -13,7 +13,9 @@
"**/android/**",
"packages/jazz-svelte/**",
"examples/*svelte*/**",
"homepage/homepage/**"
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
"homepage/homepage/**",
"**/package.json"
]
},
"formatter": {

View File

@@ -1,5 +1,22 @@
# chat-rn-expo-clerk
## 1.0.102
### Patch Changes
- jazz-expo@0.13.10
- jazz-tools@0.13.10
- jazz-react-native-media-images@0.13.10
## 1.0.101
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-expo@0.13.9
- jazz-react-native-media-images@0.13.9
## 1.0.100
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-expo-clerk",
"main": "index.js",
"version": "1.0.100",
"version": "1.0.102",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,5 +1,20 @@
# chat-rn-expo
## 1.0.89
### Patch Changes
- jazz-expo@0.13.10
- jazz-tools@0.13.10
## 1.0.88
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-expo@0.13.9
## 1.0.87
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn-expo",
"version": "1.0.87",
"version": "1.0.89",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,5 +1,23 @@
# chat-rn
## 1.0.97
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-transport-ws@0.13.10
- jazz-react-native@0.13.10
- jazz-tools@0.13.10
## 1.0.96
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react-native@0.13.9
## 1.0.95
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.95",
"version": "1.0.97",
"main": "index.js",
"scripts": {
"android": "react-native run-android",

View File

@@ -1,5 +1,22 @@
# chat-vue
## 0.0.81
### Patch Changes
- jazz-browser@0.13.10
- jazz-tools@0.13.10
- jazz-vue@0.13.10
## 0.0.80
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-browser@0.13.9
- jazz-vue@0.13.9
## 0.0.79
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.79",
"version": "0.0.81",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,22 @@
# jazz-example-chat
## 0.0.179
### Patch Changes
- jazz-inspector@0.13.10
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.178
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-inspector@0.13.9
- jazz-react@0.13.9
## 0.0.177
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.177",
"version": "0.0.179",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,22 @@
# minimal-auth-clerk
## 0.0.78
### Patch Changes
- jazz-react@0.13.10
- jazz-react-auth-clerk@0.13.10
- jazz-tools@0.13.10
## 0.0.77
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
- jazz-react-auth-clerk@0.13.9
## 0.0.76
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.76",
"version": "0.0.78",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# file-share-svelte
## 0.0.61
### Patch Changes
- jazz-svelte@0.13.10
- jazz-tools@0.13.10
## 0.0.60
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-svelte@0.13.9
## 0.0.59
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.59",
"version": "0.0.61",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,22 @@
# jazz-tailwind-demo-auth-starter
## 0.0.18
### Patch Changes
- jazz-inspector@0.13.10
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.17
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-inspector@0.13.9
- jazz-react@0.13.9
## 0.0.16
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "filestream",
"private": true,
"version": "0.0.16",
"version": "0.0.18",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# form
## 0.1.19
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.1.18
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.1.17
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.1.17",
"version": "0.1.19",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# image-upload
## 0.0.75
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.74
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.73
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.73",
"version": "0.0.75",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# jazz-example-inspector
## 0.0.129
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-transport-ws@0.13.10
- jazz-inspector@0.13.10
## 0.0.128
### Patch Changes
- jazz-inspector@0.13.9
## 0.0.127
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.127",
"version": "0.0.129",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -0,0 +1,2 @@
VITE_JAZZ_WORKER_ACCOUNT=
JAZZ_WORKER_SECRET=

View File

@@ -0,0 +1,3 @@
dist
.env

View File

@@ -0,0 +1,8 @@
# Ignore artifacts:
build
coverage
**/.git
**/.svn
**/.hg
**/node_modules

View File

@@ -0,0 +1,24 @@
# Jazz Paper Scissors
## Setup
First of we need to create a new account for the dealer:
```bash
pnpm generate-env
```
This will generate a .env file like this one
```
VITE_JAZZ_WORKER_ACCOUNT=co_zn95yzQd1z24DJCgayN53ShyuMR
JAZZ_WORKER_SECRET=sealerSecret_z3Tcq41gtELJRHk3SzQutR2DhkpvEScQQP8DG8yeSh7zJ/signerSecret_zDsLhoNRSxjXrX6oSGzGH3XQQHDyp8QS292p28RToANYq
```
This should be enough the setup everything
Then run pnpm dev to start both the local build and the worker
```bash
pnpm dev
```

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,21 @@
import * as fs from "fs";
import { createWorkerAccount } from "jazz-run/createWorkerAccount";
if (fs.existsSync(".env")) {
process.exit(0);
}
const account = await createWorkerAccount({
name: "jazz-paper-scissors-worker",
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
});
fs.writeFileSync(
".env",
`
VITE_JAZZ_WORKER_ACCOUNT=${account.accountID}
JAZZ_WORKER_SECRET=${account.agentSecret}
`,
);
process.exit(0);

View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-tsrouter-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Jazz example - Jazz Paper Scissors</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,44 @@
{
"name": "jazz-paper-scissors",
"private": true,
"type": "module",
"scripts": {
"dev": "npm-run-all --parallel dev:web dev:worker",
"dev:web": "vite",
"dev:worker": "tsx --watch --env-file=.env ./src/worker.ts",
"build": "vite build && tsc",
"serve": "vite preview",
"generate-env": "tsx generate-env.ts"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tailwindcss/vite": "^4.0.17",
"@tanstack/react-router": "^1.115.0",
"@tanstack/react-router-devtools": "^1.114.29",
"@tanstack/router-plugin": "^1.114.30",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-inspector": "workspace:*",
"jazz-nodejs": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.485.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.17",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"jazz-run": "workspace:*",
"npm-run-all": "^4.1.5",
"tsx": "^4.19.3",
"typescript": "~5.6.2",
"vite": "6.0.11"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,15 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,33 @@
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { useAccount } from "jazz-react";
import { routeTree } from "./routeTree.gen";
// Create a new router instance
const router = createRouter({
routeTree,
context: {
// @ts-expect-error - just a placeholder - me is set in the App component down below
me: undefined,
},
defaultPreload: "intent",
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
});
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
export function App() {
const { me } = useAccount();
if (!me) {
return <div>Loading...</div>;
}
return <RouterProvider router={router} context={{ me }} />;
}

View File

@@ -0,0 +1,79 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
import { Loader2Icon } from "lucide-react";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
interface Props
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
loadingText?: string;
}
function Button({
className,
variant,
size,
asChild = false,
loading = false,
disabled,
loadingText = "Loading...",
children,
...props
}: Props) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
disabled={loading || disabled}
{...props}
>
{loading ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
{loadingText}
</>
) : (
children
)}
</Comp>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,109 @@
import type * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardSmall({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-small"
className={cn(
"sm-card grid grid-flow-col justify-items-center gap-6 rounded-xl border py-6 shadow-sm m-4",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center px-6 [.border-t]:pt-6 border-t",
className,
)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
CardSmall,
};

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,13 @@
import { JazzProvider } from "jazz-react";
export function JazzAndAuth({ children }: { children: React.ReactNode }) {
return (
<JazzProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
}}
>
{children}
</JazzProvider>
);
}

View File

@@ -0,0 +1,22 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,28 @@
"use client";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,5 @@
if (!import.meta.env.VITE_JAZZ_WORKER_ACCOUNT) {
throw new Error(".env missing, run `pnpm generate-env`");
}
export const WORKER_ID = import.meta.env.VITE_JAZZ_WORKER_ACCOUNT;

View File

@@ -0,0 +1,120 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,23 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Given a player selections, returns the winner of the current game.
*/
export function determineWinner(player1Choice: string, player2Choice: string) {
if (player1Choice === player2Choice) {
return "draw";
} else if (
(player1Choice === "rock" && player2Choice === "scissors") ||
(player1Choice === "paper" && player2Choice === "rock") ||
(player1Choice === "scissors" && player2Choice === "paper")
) {
return "player1";
} else {
return "player2";
}
}

View File

@@ -0,0 +1,24 @@
import { JazzInspector } from "jazz-inspector";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { JazzProvider } from "jazz-react";
import { App } from "./app";
const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<JazzProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
}}
>
<JazzInspector />
<App />
</JazzProvider>
</StrictMode>,
);
}

View File

@@ -0,0 +1,177 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from "./routes/__root";
import { Route as AuthenticatedImport } from "./routes/_authenticated";
import { Route as IndexImport } from "./routes/index";
import { Route as AuthenticatedWaitingRoomWaitingRoomIdImport } from "./routes/_authenticated/waiting-room.$waitingRoomId";
import { Route as AuthenticatedGameGameIdImport } from "./routes/_authenticated/game.$gameId";
// Create/Update Routes
const AuthenticatedRoute = AuthenticatedImport.update({
id: "/_authenticated",
getParentRoute: () => rootRoute,
} as any);
const IndexRoute = IndexImport.update({
id: "/",
path: "/",
getParentRoute: () => rootRoute,
} as any);
const AuthenticatedWaitingRoomWaitingRoomIdRoute =
AuthenticatedWaitingRoomWaitingRoomIdImport.update({
id: "/waiting-room/$waitingRoomId",
path: "/waiting-room/$waitingRoomId",
getParentRoute: () => AuthenticatedRoute,
} as any);
const AuthenticatedGameGameIdRoute = AuthenticatedGameGameIdImport.update({
id: "/game/$gameId",
path: "/game/$gameId",
getParentRoute: () => AuthenticatedRoute,
} as any);
// Populate the FileRoutesByPath interface
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
"/_authenticated": {
id: "/_authenticated";
path: "";
fullPath: "";
preLoaderRoute: typeof AuthenticatedImport;
parentRoute: typeof rootRoute;
};
"/_authenticated/game/$gameId": {
id: "/_authenticated/game/$gameId";
path: "/game/$gameId";
fullPath: "/game/$gameId";
preLoaderRoute: typeof AuthenticatedGameGameIdImport;
parentRoute: typeof AuthenticatedImport;
};
"/_authenticated/waiting-room/$waitingRoomId": {
id: "/_authenticated/waiting-room/$waitingRoomId";
path: "/waiting-room/$waitingRoomId";
fullPath: "/waiting-room/$waitingRoomId";
preLoaderRoute: typeof AuthenticatedWaitingRoomWaitingRoomIdImport;
parentRoute: typeof AuthenticatedImport;
};
}
}
// Create and export the route tree
interface AuthenticatedRouteChildren {
AuthenticatedGameGameIdRoute: typeof AuthenticatedGameGameIdRoute;
AuthenticatedWaitingRoomWaitingRoomIdRoute: typeof AuthenticatedWaitingRoomWaitingRoomIdRoute;
}
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedGameGameIdRoute: AuthenticatedGameGameIdRoute,
AuthenticatedWaitingRoomWaitingRoomIdRoute:
AuthenticatedWaitingRoomWaitingRoomIdRoute,
};
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
AuthenticatedRouteChildren,
);
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"": typeof AuthenticatedRouteWithChildren;
"/game/$gameId": typeof AuthenticatedGameGameIdRoute;
"/waiting-room/$waitingRoomId": typeof AuthenticatedWaitingRoomWaitingRoomIdRoute;
}
export interface FileRoutesByTo {
"/": typeof IndexRoute;
"": typeof AuthenticatedRouteWithChildren;
"/game/$gameId": typeof AuthenticatedGameGameIdRoute;
"/waiting-room/$waitingRoomId": typeof AuthenticatedWaitingRoomWaitingRoomIdRoute;
}
export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
"/_authenticated": typeof AuthenticatedRouteWithChildren;
"/_authenticated/game/$gameId": typeof AuthenticatedGameGameIdRoute;
"/_authenticated/waiting-room/$waitingRoomId": typeof AuthenticatedWaitingRoomWaitingRoomIdRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/" | "" | "/game/$gameId" | "/waiting-room/$waitingRoomId";
fileRoutesByTo: FileRoutesByTo;
to: "/" | "" | "/game/$gameId" | "/waiting-room/$waitingRoomId";
id:
| "__root__"
| "/"
| "/_authenticated"
| "/_authenticated/game/$gameId"
| "/_authenticated/waiting-room/$waitingRoomId";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren;
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthenticatedRoute: AuthenticatedRouteWithChildren,
};
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>();
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/_authenticated"
]
},
"/": {
"filePath": "index.tsx"
},
"/_authenticated": {
"filePath": "_authenticated.tsx",
"children": [
"/_authenticated/game/$gameId",
"/_authenticated/waiting-room/$waitingRoomId"
]
},
"/_authenticated/game/$gameId": {
"filePath": "_authenticated/game.$gameId.tsx",
"parent": "/_authenticated"
},
"/_authenticated/waiting-room/$waitingRoomId": {
"filePath": "_authenticated/waiting-room.$waitingRoomId.tsx",
"parent": "/_authenticated"
}
}
}
ROUTE_MANIFEST_END */

View File

@@ -0,0 +1,19 @@
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import type { Account } from "jazz-tools";
import { JazzAndAuth } from "../components/ui/jazz-and-auth";
interface RouterContext {
me: Account;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => (
<>
<JazzAndAuth>
<Outlet />
<TanStackRouterDevtools />
</JazzAndAuth>
</>
),
});

View File

@@ -0,0 +1,12 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated")({
component: RouteComponent,
beforeLoad: ({ context }) => {
return context;
},
});
function RouteComponent() {
return <Outlet />;
}

View File

@@ -0,0 +1,167 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardSmall,
CardTitle,
} from "@/components/ui/card";
import { WORKER_ID } from "@/constants";
import { Game, NewGameIntent, PlayIntent } from "@/schema";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { experimental_useInboxSender, useCoState } from "jazz-react";
import { type ID } from "jazz-tools";
import { Badge, CircleHelp, Scissors, ScrollText } from "lucide-react";
import { useEffect, useState } from "react";
const playIcon = (selection: string | undefined) => {
switch (selection) {
case "rock":
return <Badge className="w-5 h-5" />;
case "paper":
return <ScrollText className="w-5 h-5" />;
case "scissors":
return <Scissors className="w-5 h-5" />;
default:
return <CircleHelp className="w-5 h-5" />;
}
};
export const Route = createFileRoute("/_authenticated/game/$gameId")({
component: RouteComponent,
loader: async ({ context: { me }, params: { gameId } }) => {
const game = await Game.load(gameId as ID<Game>, {
resolve: {
player1: true,
player2: true,
},
});
if (!game) {
throw redirect({ to: "/" });
}
return { gameId, me, loaderGame: game };
},
});
function RouteComponent() {
const { gameId, me, loaderGame } = Route.useLoaderData();
const isPlayer1 = loaderGame.player1?.account?.isMe;
const player = isPlayer1 ? "player1" : "player2";
const [playSelection, setPlaySelection] = useState(
loaderGame[player]?.playSelection ?? "",
);
const sendInboxMessage = experimental_useInboxSender(WORKER_ID);
const game = useCoState(Game, gameId as ID<Game>);
useEffect(() => {
let gameCompleted = Boolean(loaderGame.outcome);
return loaderGame.subscribe((game) => {
if (gameCompleted && !game.outcome) {
setPlaySelection(""); // Reset play selection when one player clicks on "Start a new game"
}
gameCompleted = Boolean(game.outcome);
});
}, []);
if (!game) {
return null;
}
const gameComplete = game.outcome !== undefined;
const opponent = isPlayer1 ? "player2" : "player1";
const currentPlayer = game[player];
const opponentPlayer = game[opponent];
const opponentSelection = opponentPlayer?.playSelection;
const onSubmit = async (playSelection: string) => {
sendInboxMessage(
PlayIntent.create({ type: "play", gameId, player, playSelection }),
);
};
const onNewGame = async () => {
sendInboxMessage(NewGameIntent.create({ type: "newGame", gameId }));
};
return (
<Card className="mx-auto max-w-5xl">
<div className="mx-auto text-center">
<CardHeader>
<CardTitle>Jazz, Paper, Scissors!</CardTitle>
<span>Welcome {isPlayer1 ? "Player 1" : "Player 2"}</span>
<span>
{game?.player1Score ?? 0} - {game?.player2Score ?? 0}
</span>
</CardHeader>
{gameComplete ? (
<>
<div className="border">
Game Over,{" "}
{game?.outcome === player
? "You Win!"
: game?.outcome === "draw"
? "It's a Draw!"
: "You Lose!"}
</div>
<Button onClick={onNewGame}>Start a new game</Button>
</>
) : null}
<CardContent>
<div>
{playSelection === "" ? "Make Your Selection" : "Your Selection: "}
</div>
<CardSmall>{playIcon(playSelection)}</CardSmall>
{gameComplete ? null : (
<>
<dl className="grid grid-cols-3 gap-x-8 gap-y-16 text-center">
<Button
variant={"outline"}
size={"icon"}
onClick={() => setPlaySelection("rock")}
>
<Badge className="w-5 h-5" />
</Button>
<Button
variant={"outline"}
size={"icon"}
onClick={() => setPlaySelection("paper")}
>
<ScrollText className="w-5 h-5" />
</Button>
<Button
variant={"outline"}
size={"icon"}
onClick={() => setPlaySelection("scissors")}
>
<Scissors className="w-5 h-5" />
</Button>
</dl>
<div className="m-4">
<Button
disabled={
playSelection === "" ||
Boolean(currentPlayer?.playSelection)
}
onClick={() => onSubmit(playSelection)}
>
Go!
</Button>
</div>
</>
)}
<div>Your Opponent Selected:</div>
<CardSmall>{playIcon(opponentSelection)}</CardSmall>
</CardContent>
</div>
</Card>
);
}

View File

@@ -0,0 +1,120 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { WORKER_ID } from "@/constants";
import { JoinGameRequest, WaitingRoom } from "@/schema";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { Group, type ID, InboxSender } from "jazz-tools";
import { ClipboardCopyIcon, Loader2Icon } from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute(
"/_authenticated/waiting-room/$waitingRoomId",
)({
component: RouteComponent,
loader: async ({ context: { me }, params: { waitingRoomId } }) => {
const waitingRoom = await WaitingRoom.load(
waitingRoomId as ID<WaitingRoom>,
{
resolve: {
game: true,
},
},
);
if (!waitingRoom) {
throw redirect({ to: "/" });
}
if (!waitingRoom?.account1?.isMe) {
const sender = await InboxSender.load<JoinGameRequest, WaitingRoom>(
WORKER_ID,
me,
// { account1: {}, account2: {}, me, game: {} },
);
sender.sendMessage(
JoinGameRequest.create(
{ type: "joinGame", waitingRoom },
{ owner: Group.create({ owner: me }) },
),
);
// If the waiting room already has a game, redirect to the game
if (waitingRoom?.game) {
throw redirect({ to: `/game/${waitingRoom.game.id}` as string });
}
}
return { waitingRoom };
},
});
function RouteComponent() {
const { waitingRoom } = Route.useLoaderData();
const navigate = Route.useNavigate();
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!waitingRoom) {
return;
}
return waitingRoom.subscribe(
{
resolve: {
game: true,
},
},
async () => {
if (waitingRoom.game) {
navigate({ to: `/game/${waitingRoom.game.id}` });
}
},
);
}, [waitingRoom]);
const onCopyClick = () => {
navigator.clipboard.writeText(window.location.toString());
setCopied(true);
};
return (
<div className="h-screen flex flex-col w-full place-items-center justify-center p-2">
<Card className="w-[500px]">
<CardHeader>
<CardTitle className="flex items-center">
<Loader2Icon className="animate-spin inline h-8 w-8 mr-2" />
Waiting for opponent to join the game
</CardTitle>
<CardDescription>
Share this link with your friend to join the game. The game will
automatically start once they join.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex">
<Input
className="w-full border bg-muted rounded-e-none"
readOnly
value={`${window.location}`}
/>
<Button onClick={onCopyClick} className="rounded-s-none w-25">
{copied ? (
"Copied!"
) : (
<>
<ClipboardCopyIcon className="w-5 h-5" />
Copy
</>
)}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { WORKER_ID } from "@/constants";
import { CreateGameRequest } from "@/schema";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { experimental_useInboxSender as useInboxSender } from "jazz-react";
import { useState } from "react";
export const Route = createFileRoute("/")({
component: HomeComponent,
});
function HomeComponent() {
const createGame = useInboxSender(WORKER_ID);
const navigate = useNavigate({ from: "/" });
const [isLoading, setIsLoading] = useState(false);
const onNewGameClick = async () => {
setIsLoading(true);
const waitingRoomId = await createGame(
CreateGameRequest.create({
type: "createGame",
}),
);
if (!waitingRoomId) {
setIsLoading(false);
return;
}
navigate({ to: `/waiting-room/$waitingRoomId`, params: { waitingRoomId } });
};
return (
<div className="h-screen flex flex-col w-full place-items-center justify-center p-2">
<Card className="w-[500px]">
<CardHeader>
<CardTitle>Welcome to Jazz, Paper, Scissors!</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="flex items-center p-4">
<Button
onClick={onNewGameClick}
loading={isLoading}
loadingText="Creating game..."
className="w-full"
>
New Game
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { Account, CoMap, SchemaUnion, co } from "jazz-tools";
export class Game extends CoMap {
player1 = co.ref(Player);
player2? = co.ref(Player);
outcome? = co.literal("player1", "player2", "draw");
player1Score = co.number;
player2Score = co.number;
/**
* Given a player, returns the opponent in the current game.
*/
getOpponent(player: Player) {
// TODO: player may be unrelated to this game
const opponent =
player.account?.id === this.player1?.account?.id
? this.player2
: this.player1;
if (!opponent) {
throw new Error("Opponent not found");
}
return opponent.ensureLoaded({
// account: {},
resolve: {},
});
}
}
export class Player extends CoMap {
account = co.ref(Account);
playSelection? = co.string;
}
export class WaitingRoom extends CoMap {
account1 = co.ref(Account);
account2 = co.optional.ref(Account);
game = co.optional.ref(Game);
}
export class InboxMessage extends CoMap {
type = co.literal("play", "createGame", "joinGame", "newGame");
}
export class PlayIntent extends InboxMessage {
type = co.literal("play");
gameId = co.string;
player = co.literal("player1", "player2");
playSelection = co.string;
}
export class NewGameIntent extends InboxMessage {
type = co.literal("newGame");
gameId = co.string;
}
export class CreateGameRequest extends InboxMessage {
type = co.literal("createGame");
}
export class JoinGameRequest extends InboxMessage {
type = co.literal("joinGame");
waitingRoom = co.ref(WaitingRoom);
}

View File

@@ -0,0 +1,201 @@
import {
Game,
InboxMessage,
JoinGameRequest,
NewGameIntent,
PlayIntent,
Player,
WaitingRoom,
} from "@/schema";
import { startWorker } from "jazz-nodejs";
import { Account, Group, type ID } from "jazz-tools";
import { determineWinner } from "./lib/utils";
if (!process.env.VITE_JAZZ_WORKER_ACCOUNT || !process.env.JAZZ_WORKER_SECRET) {
throw new Error(".env missing, run `pnpm generate-env`");
}
const {
worker,
experimental: { inbox },
} = await startWorker({
accountID: process.env.VITE_JAZZ_WORKER_ACCOUNT,
syncServer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co ",
});
inbox.subscribe(
InboxMessage,
async (message, senderID) => {
const playerAccount = await Account.load(senderID, { loadAs: worker });
if (!playerAccount) {
return;
}
switch (message.type) {
case "play":
handlePlayIntent(senderID, message.castAs(PlayIntent));
break;
case "newGame":
handleNewGameIntent(senderID, message.castAs(NewGameIntent));
break;
case "createGame":
const waitingRoomGroup = Group.create({ owner: worker });
waitingRoomGroup.addMember("everyone", "reader");
const waitingRoom = WaitingRoom.create(
{ account1: playerAccount },
{ owner: waitingRoomGroup },
);
console.log("waiting room created with id:", waitingRoom.id);
return waitingRoom;
case "joinGame":
const joinGameRequest = message.castAs(JoinGameRequest);
if (
!joinGameRequest.waitingRoom ||
!joinGameRequest.waitingRoom.account1
) {
console.error("No waiting room in join game request");
return;
}
joinGameRequest.waitingRoom.account2 = playerAccount;
const game = await createGame({
account1: joinGameRequest.waitingRoom.account1,
account2: joinGameRequest.waitingRoom.account2,
});
console.log("game created with id:", game.id);
joinGameRequest.waitingRoom.game = game;
return joinGameRequest.waitingRoom.game;
}
},
{ retries: 3 },
);
console.log("worker", worker.id, "started");
interface CreateGameParams {
account1: Account;
account2: Account;
}
async function createGame({ account1, account2 }: CreateGameParams) {
const publicReadOnly = Group.create({ owner: worker });
publicReadOnly.addMember(account1, "reader");
publicReadOnly.addMember(account2, "reader");
const player1 = createPlayer({ account: account1 });
const player2 = createPlayer({ account: account2 });
const game = Game.create(
{
player1: player1,
player2: player2,
player1Score: 0,
player2Score: 0,
},
{ owner: publicReadOnly },
);
await game.waitForSync();
return game;
}
interface CreatePlayerParams {
account: Account;
}
function createPlayer({ account }: CreatePlayerParams) {
const publicRead = Group.create({ owner: worker });
publicRead.addMember("everyone", "reader");
const player = Player.create(
{
account,
},
{ owner: publicRead },
);
return player;
}
async function handleNewGameIntent(_: ID<Account>, message: NewGameIntent) {
const gameId = message.gameId;
const game = await Game.load(gameId as ID<Game>, {
loadAs: worker,
resolve: {
player1: true,
player2: true,
},
});
if (!game) {
throw new Error("Game not found");
}
if (game.outcome) {
game.outcome = undefined;
game.player1.playSelection = undefined;
if (game.player2) {
game.player2.playSelection = undefined;
}
}
}
async function handlePlayIntent(_: ID<Account>, message: PlayIntent) {
// determine current player, update game with outcome
const gameId = message.gameId;
if (!gameId) {
console.error("Game not found");
return;
}
const game = await Game.load(gameId as ID<Game>, {
loadAs: worker,
resolve: {
player1: true,
player2: true,
},
});
if (!game) {
throw new Error("Game not found");
}
const currentPlayer = game[message.player as "player1" | "player2"];
if (!currentPlayer) {
throw new Error("Player not found");
}
if (currentPlayer.playSelection) {
throw new Error("Player already made a selection");
}
currentPlayer.playSelection = message.playSelection;
const player1Selection = game?.player1.playSelection;
const player2Selection = game?.player2?.playSelection;
// once both players have a selection, determine the winner
if (
!!player1Selection &&
player1Selection !== "" &&
!!player2Selection &&
player2Selection !== ""
) {
const outcome = determineWinner(player1Selection, player2Selection);
game.outcome = outcome;
if (outcome === "player1") {
game.player1Score += 1;
} else if (outcome === "player2") {
game.player2Score += 1;
}
}
}

View File

@@ -0,0 +1,28 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,18 @@
import { resolve } from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
TanStackRouterVite({ autoCodeSplitting: true }),
viteReact(),
tailwindcss(),
],
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});

View File

@@ -1,5 +1,20 @@
# multi-cursors
## 0.0.71
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.70
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.69
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multi-cursors",
"private": true,
"version": "0.0.69",
"version": "0.0.71",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,22 @@
# multiauth
## 0.0.19
### Patch Changes
- jazz-react@0.13.10
- jazz-react-auth-clerk@0.13.10
- jazz-tools@0.13.10
## 0.0.18
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
- jazz-react-auth-clerk@0.13.9
## 0.0.17
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "multiauth",
"private": true,
"version": "0.0.17",
"version": "0.0.19",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,22 @@
# jazz-example-musicplayer
## 0.0.100
### Patch Changes
- jazz-inspector@0.13.10
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.99
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-inspector@0.13.9
- jazz-react@0.13.9
## 0.0.98
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.98",
"version": "0.0.100",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# organization
## 0.0.71
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.70
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.69
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.69",
"version": "0.0.71",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# passkey-svelte
## 0.0.65
### Patch Changes
- jazz-svelte@0.13.10
- jazz-tools@0.13.10
## 0.0.64
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-svelte@0.13.9
## 0.0.63
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.63",
"version": "0.0.65",
"type": "module",
"private": true,
"scripts": {
@@ -32,6 +32,7 @@
"vite": "6.0.11"
},
"dependencies": {
"jazz-svelte": "workspace:*"
"jazz-svelte": "workspace:*",
"jazz-tools": "workspace:*"
}
}

View File

@@ -1,5 +1,20 @@
# minimal-auth-passkey
## 0.0.76
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.75
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.74
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.74",
"version": "0.0.76",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# passphrase
## 0.0.73
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.72
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.71
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passphrase",
"private": true,
"version": "0.0.71",
"version": "0.0.73",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# jazz-password-manager
## 0.0.97
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.96
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.95
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.95",
"version": "0.0.97",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# jazz-example-pets
## 0.0.195
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.194
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.193
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.193",
"version": "0.0.195",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,20 @@
# reactions
## 0.0.75
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.74
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.73
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.73",
"version": "0.0.75",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,22 @@
# todo-vue
## 0.0.79
### Patch Changes
- jazz-browser@0.13.10
- jazz-tools@0.13.10
- jazz-vue@0.13.10
## 0.0.78
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-browser@0.13.9
- jazz-vue@0.13.9
## 0.0.77
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.77",
"version": "0.0.79",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,20 @@
# jazz-example-todo
## 0.0.194
### Patch Changes
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.193
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-react@0.13.9
## 0.0.192
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.192",
"version": "0.0.194",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,22 @@
# version-history
## 0.0.73
### Patch Changes
- jazz-inspector@0.13.10
- jazz-react@0.13.10
- jazz-tools@0.13.10
## 0.0.72
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-inspector@0.13.9
- jazz-react@0.13.9
## 0.0.71
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.71",
"version": "0.0.73",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,13 @@
# cojson-storage-indexeddb
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-storage@0.13.10
## 0.13.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.13.7",
"version": "0.13.10",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-storage@0.13.10
## 0.13.7
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.13.7",
"version": "0.13.10",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.13.7",
"cojson": "workspace:0.13.10",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -1,5 +1,12 @@
# cojson-storage
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
## 0.13.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage",
"version": "0.13.7",
"version": "0.13.10",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,12 @@
# cojson-transport-nodejs-ws
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
## 0.13.7
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.13.7",
"version": "0.13.10",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -1,5 +1,11 @@
# cojson
## 0.13.10
### Patch Changes
- f837cfe: Fix fatal errors happening when loading a CoValueCore without having the dependencies
## 0.13.7
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.13.7",
"version": "0.13.10",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"typescript": "catalog:"

View File

@@ -121,14 +121,35 @@ export class CoValueCore {
this.header = header;
this._sessionLogs = internalInitSessions;
this.node = node;
}
groupInvalidationSubscription?: () => void;
subscribeToGroupInvalidation() {
if (this.groupInvalidationSubscription) {
return;
}
const header = this.header;
if (header.ruleset.type == "ownedByGroup") {
this.node
.expectCoValueLoaded(header.ruleset.group)
.subscribe((_groupUpdate) => {
this._cachedContent = undefined;
this.notifyUpdate("immediate");
const groupId = header.ruleset.group;
const entry = this.node.coValuesStore.get(groupId);
if (entry.state.type === "available") {
this.groupInvalidationSubscription = entry.state.coValue.subscribe(
(_groupUpdate) => {
this._cachedContent = undefined;
this.notifyUpdate("immediate");
},
false,
);
} else {
logger.error("CoValueCore: Owner group not available", {
id: this.id,
groupId,
});
}
}
}
@@ -368,9 +389,15 @@ export class CoValueCore {
}
}
subscribe(listener: (content?: RawCoValue) => void): () => void {
subscribe(
listener: (content?: RawCoValue) => void,
immediateInvoke = true,
): () => void {
this.listeners.add(listener);
listener(this.getCurrentContent());
if (immediateInvoke) {
listener(this.getCurrentContent());
}
return () => {
this.listeners.delete(listener);
@@ -474,6 +501,8 @@ export class CoValueCore {
return this._cachedContent;
}
this.subscribeToGroupInvalidation();
const newContent = coreToCoValue(this, options);
if (!options?.ignorePrivateTransactions) {

View File

@@ -287,12 +287,30 @@ test("creating a coValue with a group should't trigger automatically a content c
});
// It's called once for the group and never for the coValue
expect(getCurrentContentSpy).toHaveBeenCalledTimes(1);
expect(groupSpy).toHaveBeenCalledTimes(1);
expect(getCurrentContentSpy).toHaveBeenCalledTimes(0);
expect(groupSpy).toHaveBeenCalledTimes(0);
getCurrentContentSpy.mockRestore();
});
test("loading a coValue core without having the owner group available doesn't crash", () => {
const [account, sessionID] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(account, sessionID, Crypto);
const otherNode = createTestNode();
const group = otherNode.createGroup();
const coValue = node.createCoValue({
type: "costream",
ruleset: { type: "ownedByGroup", group: group.id },
meta: null,
...Crypto.createdNowUnique(),
});
expect(coValue.id).toBeDefined();
});
test("listeners are notified even if the previous listener threw an error", async () => {
const { node1, node2 } = await createTwoConnectedNodes("server", "server");

View File

@@ -1,5 +1,22 @@
# jazz-auth-clerk
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- jazz-browser@0.13.10
- jazz-tools@0.13.10
## 0.13.9
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-browser@0.13.9
## 0.13.8
### Patch Changes

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-auth-clerk",
"version": "0.13.8",
"version": "0.13.10",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.13.7",
"jazz-browser": "workspace:0.13.7",
"jazz-tools": "workspace:0.13.7"
"cojson": "workspace:0.13.10",
"jazz-browser": "workspace:0.13.10",
"jazz-tools": "workspace:0.13.10"
},
"scripts": {
"format-and-lint": "biome check .",

View File

@@ -1,5 +1,20 @@
# jazz-browser-media-images
## 0.13.10
### Patch Changes
- jazz-browser@0.13.10
- jazz-tools@0.13.10
## 0.13.9
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-browser@0.13.9
## 0.13.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser-media-images",
"version": "0.13.7",
"version": "0.13.10",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -8,8 +8,8 @@
"dependencies": {
"@types/image-blob-reduce": "^4.1.1",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "workspace:0.13.7",
"jazz-tools": "workspace:0.13.7",
"jazz-browser": "workspace:0.13.10",
"jazz-tools": "workspace:0.13.10",
"pica": "^9.0.1"
},
"scripts": {

View File

@@ -1,5 +1,22 @@
# jazz-browser
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-storage-indexeddb@0.13.10
- cojson-transport-ws@0.13.10
- jazz-tools@0.13.10
## 0.13.9
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
## 0.13.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser",
"version": "0.13.7",
"version": "0.13.10",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,27 @@
# jazz-browser
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
- cojson-transport-ws@0.13.10
- jazz-auth-clerk@0.13.10
- jazz-react-core@0.13.10
- jazz-react-native-core@0.13.10
- jazz-tools@0.13.10
## 0.13.9
### Patch Changes
- Updated dependencies [a6cf01f]
- jazz-tools@0.13.9
- jazz-auth-clerk@0.13.9
- jazz-react-core@0.13.9
- jazz-react-native-core@0.13.9
## 0.13.8
### Patch Changes

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