Compare commits

..

20 Commits

Author SHA1 Message Date
Giordano Ricci
c1193c3c63 add explainer for counters restarts 2025-04-08 11:43:48 +01:00
Giordano Ricci
f30130b92f reduce possible priority values 2025-04-08 11:43:48 +01:00
Giordano Ricci
1a77233ecb force metric creation on queue creation 2025-04-08 11:43:48 +01:00
Giordano Ricci
09a95b8542 move queue initialization into constructor 2025-04-08 11:43:48 +01:00
Giordano Ricci
f46329ac68 move meter 2025-04-08 11:43:48 +01:00
Giordano Ricci
c551839179 extract queue meter logic, add tests 2025-04-08 11:43:47 +01:00
Giordano Ricci
a0683f9d21 add role attribute to queue metrics 2025-04-08 11:43:47 +01:00
Giordano Ricci
a56958c69e WIP: queue counters 2025-04-08 11:43:47 +01:00
Guido D'Orsi
9f336103e8 Merge pull request #1820 from garden-co/changeset-release/main
Version Packages
2025-04-07 21:13:46 +02:00
github-actions[bot]
1174942a4e Version Packages 2025-04-07 19:12:40 +00:00
Guido D'Orsi
267920b7dc Merge pull request #1819 from garden-co/re-export-clerk
fix: add exports for clerk and crypto to make import shortcuts work everywhere
2025-04-07 21:10:16 +02:00
Guido D'Orsi
63a7aa0f54 fix: add exports for clerk and crypto to make import shortcuts work everywhere 2025-04-07 20:51:43 +02:00
Benjamin S. Leveritt
2b7534d30c Merge pull request #1808 from garden-co/docs/add-multicursor-example
Docs for multicursor example
2025-04-07 16:03:49 +01:00
Trisha Lim
be23a81b1b fix: create-jazz-app command showing $EXAMPLE instead of app slug 2025-04-07 19:55:03 +07:00
Guido D'Orsi
68416784fd docs(expo-clerk): fix outdated storage property 2025-04-07 14:39:49 +02:00
Guido D'Orsi
84d3c09cb1 docs: remove the nativewind section 2025-04-07 14:38:41 +02:00
Guido D'Orsi
53e8c39e8d docs: fix the clerk expo import 2025-04-07 14:36:46 +02:00
Trisha Lim
87b41fefad add multicursors example to cofeeds docs 2025-04-07 18:56:35 +07:00
Trisha Lim
265a4e8cc5 update readme for multi-cursors example 2025-04-07 18:53:03 +07:00
Trisha Lim
623467503f add multicursors example to examples page 2025-04-07 18:33:50 +07:00
31 changed files with 414 additions and 134 deletions

View File

@@ -0,0 +1,5 @@
---
"cojson": patch
---
Add jazz.messagequeue.pushed/pulled counters, remove jazz.messagequeue.size gauge

View File

@@ -1,5 +1,12 @@
# chat-rn-expo-clerk # chat-rn-expo-clerk
## 1.0.94
### Patch Changes
- Updated dependencies [63a7aa0]
- jazz-expo@0.13.1
## 1.0.93 ## 1.0.93
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,6 +1,5 @@
import { useClerk } from "@clerk/clerk-expo"; import { useClerk } from "@clerk/clerk-expo";
// FIXME: why isn't the export working? IDE is fine, Metro doesn't like the non 'dist' import import { JazzProviderWithClerk } from "jazz-expo/auth/clerk";
import { JazzProviderWithClerk } from "jazz-expo/dist/auth/clerk";
import React, { PropsWithChildren } from "react"; import React, { PropsWithChildren } from "react";
import { apiKey } from "./apiKey"; import { apiKey } from "./apiKey";

View File

@@ -1,5 +1,12 @@
# chat-rn-expo # chat-rn-expo
## 1.0.81
### Patch Changes
- Updated dependencies [63a7aa0]
- jazz-expo@0.13.1
## 1.0.80 ## 1.0.80
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,5 +1,12 @@
# chat-rn # chat-rn
## 1.0.90
### Patch Changes
- Updated dependencies [63a7aa0]
- jazz-react-native@0.13.1
## 1.0.89 ## 1.0.89
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,3 +1,64 @@
# Multi-cursor example # Jazz Multi-Cursors Example
An example app of using Jazz for showing multiple-cursors on a simple canvas. Track user presence on a canvas with multiple cursors and out of bounds indicators.
## Getting started
You can either
1. Clone the jazz repository, and run the app within the monorepo.
2. Or create a new Jazz project using this example as a template.
### Using the example as a template
Create a new Jazz project, and use this example as a template.
```bash
npx create-jazz-app@latest multi-cursors-app --example multi-cursors
```
Go to the new project directory.
```bash
cd multi-cursors-app
```
Run the dev server.
```bash
npm run dev
```
### Using the monorepo
This requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation).
Clone the jazz repository.
```bash
git clone https://github.com/garden-co/jazz.git
```
Install and build dependencies.
```bash
pnpm i && npx turbo build
```
Go to the example directory.
```bash
cd jazz/examples/multi-cursors/
```
Start the dev server.
```bash
pnpm dev
```
Open [http://localhost:5173](http://localhost:5173) with your browser to see the result.
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Cloud](https://jazz.tools/cloud) (`wss://cloud.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx jazz-run sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?peer=ws://localhost:4200`), or by setting the `sync` parameter of `JazzProvider` component in [./src/main.tsx](./src/main.tsx).

View File

@@ -1,5 +1,4 @@
import { import {
AlertCircleIcon,
AlertTriangleIcon, AlertTriangleIcon,
ArrowDownIcon, ArrowDownIcon,
ArrowRightIcon, ArrowRightIcon,
@@ -27,6 +26,7 @@ import {
MessageCircleQuestionIcon, MessageCircleQuestionIcon,
MonitorSmartphoneIcon, MonitorSmartphoneIcon,
MoonIcon, MoonIcon,
MousePointer2Icon,
MousePointerSquareDashedIcon, MousePointerSquareDashedIcon,
ScanFace, ScanFace,
ScrollIcon, ScrollIcon,
@@ -50,6 +50,7 @@ const icons = {
close: XIcon, close: XIcon,
code: CodeIcon, code: CodeIcon,
copy: ClipboardIcon, copy: ClipboardIcon,
cursor: MousePointer2Icon,
darkTheme: MoonIcon, darkTheme: MoonIcon,
delete: TrashIcon, delete: TrashIcon,
devices: MonitorSmartphoneIcon, devices: MonitorSmartphoneIcon,

View File

@@ -209,13 +209,14 @@ However, once authenticated, your users won't need to interact with Clerk anymor
<ContentByFramework framework="react"> <ContentByFramework framework="react">
The clerk provider is not built into `jazz-react` and needs the `jazz-react-auth-clerk` package to be installed. The clerk provider is not built into `jazz-react` and needs the `jazz-react-auth-clerk` package to be installed.
After installing the package you can use the `JazzProviderWithClerk` component to wrap your app:
</ContentByFramework> </ContentByFramework>
<ContentByFramework framework="react-native-expo"> <ContentByFramework framework="react-native-expo">
The clerk provider is not built into `jazz-expo` and needs the `jazz-expo/auth/clerk` import to be installed. Note the `__experimental_resourceCache` option. This helps render Clerk components when offline. You can use the `JazzProviderWithClerk` component to wrap your app. Note the `__experimental_resourceCache` option. This helps render Clerk components when offline.
</ContentByFramework> </ContentByFramework>
After installing the package you can use the `JazzProviderWithClerk` component to wrap your app:
<ContentByFramework framework="react"> <ContentByFramework framework="react">
<CodeGroup> <CodeGroup>
```tsx ```tsx

View File

@@ -79,10 +79,10 @@ If you're using Clerk auth in your Expo application, you'll need to:
```tsx twoslash ```tsx twoslash
// @noErrors: 2300 2307 // @noErrors: 2300 2307
// Before // Before
import { ClerkAuthProvider, useClerkAuth } from "jazz-react-native-clerk"; // [!code --] import { JazzProviderWithClerk } from "jazz-react-native-clerk"; // [!code --]
// After // After
import { ClerkAuthProvider, useClerkAuth } from "jazz-expo/auth/clerk"; // [!code ++] import { JazzProviderWithClerk } from "jazz-expo/auth/clerk"; // [!code ++]
``` ```
</CodeGroup> </CodeGroup>
@@ -134,31 +134,6 @@ declare module "jazz-expo" { // [!code ++:5]
The `jazz-expo` implementation supports the Expo New Architecture. The `jazz-expo` implementation supports the Expo New Architecture.
## Styling with NativeWind
For Expo projects, remember to use NativeWind CSS for styling, which is the recommended approach for Jazz applications built with Expo:
<CodeGroup>
```tsx twoslash
// @noErrors: 2305 2686
import { View, Text } from "react-native";
import { styled } from "nativewind";
const StyledView = styled(View);
const StyledText = styled(Text);
export function MyComponent() {
return (
<StyledView className="p-4 bg-white dark:bg-slate-800">
<StyledText className="text-black dark:text-white text-lg font-bold">
Hello, Jazz with Expo!
</StyledText>
</StyledView>
);
}
```
</CodeGroup>
## For More Information ## For More Information
For detailed setup instructions, refer to the [React Native Expo Setup Guide](/docs/react-native-expo/project-setup) For detailed setup instructions, refer to the [React Native Expo Setup Guide](/docs/react-native-expo/project-setup)

View File

@@ -8,7 +8,9 @@ CoFeeds are append-only data structures that track entries from different user s
Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems. Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems.
The [Reactions example](https://github.com/garden-co/jazz/tree/main/examples/reactions) demonstrates a practical use of CoFeeds. The following examples demonstrate a practical use of CoFeeds:
- [Multi-cursors](https://github.com/garden-co/jazz/tree/main/examples/multi-cursors) - track user presence on a canvas with multiple cursors and out of bounds indicators
- [Reactions](https://github.com/garden-co/jazz/tree/main/examples/reactions) - store per-user emoji reaction using a CoFeed
## Creating CoFeeds ## Creating CoFeeds

View File

@@ -253,6 +253,38 @@ const ReactionsIllustration = () => (
</div> </div>
); );
const MultiCursorIllustration = () => (
<div className="flex bg-stone-100 h-full flex-col items-center justify-center dark:bg-transparent p-4">
<div className=" bg-white md:aspect-[3/2] flex flex-col rounded-md shadow-xl shadow-stone-400/20 dark:shadow-none">
<div className="w-full py-2 flex items-center gap-1.5 px-2 border-b dark:border-b-stone-200">
<span className="rounded-full size-2 bg-stone-200"></span>
<span className="rounded-full size-2 bg-stone-200"></span>
<span className="rounded-full size-2 bg-stone-200"></span>
</div>
<div className="h-full mx-auto flex flex-col justify-center p-12 sm:p-16">
<div className="inline-block relative px-1 ring-1 ring-blue-400">
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -top-1"></div>
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -top-1"></div>
<div className="absolute size-2 bg-white border border-blue-400 -left-1 -bottom-1"></div>
<div className="absolute size-2 bg-white border border-blue-400 -right-1 -bottom-1"></div>
<span className="text-lg font-semibold md:text-2xl md:font-bold text-stone-800 ">
Hello, world!
</span>
<div className="absolute -top-10 right-4 text-rose-600 flex items-end gap-1">
<Icon name="cursor"></Icon> <span className="text-xs">Mia</span>
</div>
<div className="absolute -bottom-10 left-4 text-green-600 flex items-end gap-1">
<Icon name="cursor"></Icon>{" "}
<span className="text-xs">Sebastian</span>
</div>
</div>
</div>
</div>
</div>
);
const PetIllustration = () => ( const PetIllustration = () => (
<div className="h-full p-4 bg-[url('/dog.jpg')] bg-cover bg-center p-4 flex items-end"> <div className="h-full p-4 bg-[url('/dog.jpg')] bg-cover bg-center p-4 flex items-end">
<div className="inline-flex justify-center gap-1 mx-auto"> <div className="inline-flex justify-center gap-1 mx-auto">
@@ -395,6 +427,16 @@ const reactExamples: Example[] = [
demoUrl: "https://reactions-demo.jazz.tools", demoUrl: "https://reactions-demo.jazz.tools",
illustration: <ReactionsIllustration />, illustration: <ReactionsIllustration />,
}, },
{
name: "Cursor presence",
slug: "multi-cursors",
description:
"Track user presence on a canvas with multiple cursors and out of bounds indicators.",
tech: [tech.react],
features: [features.coFeed],
demoUrl: "https://jazz-multi-cursors.vercel.app",
illustration: <MultiCursorIllustration />,
},
{ {
name: "Rate my pet", name: "Rate my pet",
slug: "pets", slug: "pets",
@@ -484,7 +526,8 @@ const rnExamples: Example[] = [
{ {
name: "Chat", name: "Chat",
slug: "chat-rn", slug: "chat-rn",
description: "A simple React Native app that creates a chat room with a shareable link.", description:
"A simple React Native app that creates a chat room with a shareable link.",
tech: [tech.reactNative], tech: [tech.reactNative],
illustration: <ChatIllustration />, illustration: <ChatIllustration />,
}, },
@@ -492,7 +535,8 @@ const rnExamples: Example[] = [
{ {
name: "Chat", name: "Chat",
slug: "chat-rn-expo", slug: "chat-rn-expo",
description: "A simple Expo app that creates a chat room with a shareable link.", description:
"A simple Expo app that creates a chat room with a shareable link.",
tech: [tech.reactNative, tech.expo], tech: [tech.reactNative, tech.expo],
illustration: <ChatIllustration />, illustration: <ChatIllustration />,
}, },
@@ -500,7 +544,8 @@ const rnExamples: Example[] = [
{ {
name: "Chat", name: "Chat",
slug: "chat-rn-expo-clerk", slug: "chat-rn-expo-clerk",
description: "Exactly like the React Native Expo chat app, with Clerk for auth.", description:
"Exactly like the React Native Expo chat app, with Clerk for auth.",
tech: [tech.reactNative, tech.expo], tech: [tech.reactNative, tech.expo],
features: [features.clerk], features: [features.clerk],
illustration: <ClerkIllustration />, illustration: <ClerkIllustration />,

View File

@@ -56,8 +56,8 @@ export const docNavigationItems = [
"react-native": 100, "react-native": 100,
"react-native-expo": 100, "react-native-expo": 100,
}, },
} },
] ],
}, },
{ {
name: "Tools", name: "Tools",
@@ -82,7 +82,7 @@ export const docNavigationItems = [
{ {
name: "0.13.0 - React Native Split", name: "0.13.0 - React Native Split",
href: "/docs/upgrade/0-13-0", href: "/docs/upgrade/0-13-0",
done: 100 done: 100,
}, },
{ {
// upgrade guides // upgrade guides

View File

@@ -126,7 +126,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);
@@ -214,7 +214,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);
@@ -243,7 +243,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);
@@ -269,7 +269,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
const stream: SyncMessage[] = []; const stream: SyncMessage[] = [];
@@ -316,7 +316,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
const stream: SyncMessage[] = []; const stream: SyncMessage[] = [];
@@ -365,7 +365,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);
@@ -411,7 +411,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);
@@ -450,7 +450,7 @@ describe("createWebSocketPeer", () => {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
new: {}, new: {},
priority: 1, priority: 6,
}; };
void peer.outgoing.push(message1); void peer.outgoing.push(message1);

View File

@@ -10,10 +10,21 @@ import { CO_VALUE_PRIORITY } from "./priority.js";
import { Peer, SyncMessage } from "./sync.js"; import { Peer, SyncMessage } from "./sync.js";
export class PeerState { export class PeerState {
private queue: PriorityBasedMessageQueue;
constructor( constructor(
private peer: Peer, private peer: Peer,
knownStates: PeerKnownStates | undefined, knownStates: PeerKnownStates | undefined,
) { ) {
/**
* We set as default priority HIGH to handle all the messages without a
* priority property as HIGH priority.
*
* This way we consider all the non-content messsages as HIGH priority.
*/
this.queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.HIGH, {
peerRole: peer.role,
});
this.optimisticKnownStates = knownStates?.clone() ?? new PeerKnownStates(); this.optimisticKnownStates = knownStates?.clone() ?? new PeerKnownStates();
// We assume that exchanges with storage peers are always successful // We assume that exchanges with storage peers are always successful
@@ -76,13 +87,6 @@ export class PeerState {
return this.peer.role === "server" || this.peer.role === "storage"; return this.peer.role === "server" || this.peer.role === "storage";
} }
/**
* We set as default priority HIGH to handle all the messages without a
* priority property as HIGH priority.
*
* This way we consider all the non-content messsages as HIGH priority.
*/
private queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.HIGH);
private processing = false; private processing = false;
public closed = false; public closed = false;

View File

@@ -1,5 +1,5 @@
import { ValueType, metrics } from "@opentelemetry/api"; import { Counter, ValueType, metrics } from "@opentelemetry/api";
import type { CoValuePriority } from "./priority.js"; import { CO_VALUE_PRIORITY, type CoValuePriority } from "./priority.js";
import type { SyncMessage } from "./sync.js"; import type { SyncMessage } from "./sync.js";
function promiseWithResolvers<R>() { function promiseWithResolvers<R>() {
@@ -34,7 +34,7 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
? A ? A
: Tuple<T, N, [...A, T]>; : Tuple<T, N, [...A, T]>;
type QueueTuple = Tuple<LinkedList<QueueEntry>, 8>; type QueueTuple = Tuple<LinkedList<QueueEntry>, 3>;
type LinkedListNode<T> = { type LinkedListNode<T> = {
value: T; value: T;
@@ -46,6 +46,8 @@ type LinkedListNode<T> = {
* as our queues can grow very large when the system is under pressure. * as our queues can grow very large when the system is under pressure.
*/ */
export class LinkedList<T> { export class LinkedList<T> {
constructor(private meter?: QueueMeter) {}
head: LinkedListNode<T> | undefined = undefined; head: LinkedListNode<T> | undefined = undefined;
tail: LinkedListNode<T> | undefined = undefined; tail: LinkedListNode<T> | undefined = undefined;
length = 0; length = 0;
@@ -64,6 +66,7 @@ export class LinkedList<T> {
} }
this.length++; this.length++;
this.meter?.push();
} }
shift() { shift() {
@@ -82,34 +85,83 @@ export class LinkedList<T> {
this.length--; this.length--;
this.meter?.pull();
return value; return value;
} }
} }
export class PriorityBasedMessageQueue { class QueueMeter {
private queues: QueueTuple = [ private pullCounter: Counter;
new LinkedList<QueueEntry>(), private pushCounter: Counter;
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
new LinkedList<QueueEntry>(),
];
queueSizeCounter = metrics
.getMeter("cojson")
.createUpDownCounter("jazz.messagequeue.size", {
description: "Size of the message queue",
valueType: ValueType.INT,
unit: "entry",
});
private getQueue(priority: CoValuePriority) { constructor(
return this.queues[priority]; prefix: string,
private attrs?: Record<string, string | number>,
) {
this.pullCounter = metrics
.getMeter("cojosn")
.createCounter(`${prefix}.pulled`, {
description: "Number of messages pulled from the queue",
valueType: ValueType.INT,
unit: "1",
});
this.pushCounter = metrics
.getMeter("cojosn")
.createCounter(`${prefix}.pushed`, {
description: "Number of messages pushed to the queue",
valueType: ValueType.INT,
unit: "1",
});
/**
* This makes sure that those metrics are generated (and emitted) as soon as the queue is created.
* This is to avoid edge cases where one series reset is delayed, which would cause spikes or dips
* when queried - and it also more correctly represents the actual state of the queue after a restart.
*/
this.pullCounter.add(0, this.attrs);
this.pushCounter.add(0, this.attrs);
} }
constructor(private defaultPriority: CoValuePriority) {} public pull() {
this.pullCounter.add(1, this.attrs);
}
public push() {
this.pushCounter.add(1, this.attrs);
}
}
function meteredList<T>(attrs?: Record<string, string | number>) {
return new LinkedList<T>(new QueueMeter("jazz.messagequeue", attrs));
}
const PRIORITY_TO_QUEUE_INDEX = {
[CO_VALUE_PRIORITY.HIGH]: 0,
[CO_VALUE_PRIORITY.MEDIUM]: 1,
[CO_VALUE_PRIORITY.LOW]: 2,
} as const;
export class PriorityBasedMessageQueue {
private queues: QueueTuple;
constructor(
private defaultPriority: CoValuePriority,
/**
* Optional attributes to be added to the generated metrics.
* By default the metrics will have the priority as an attribute.
*/
attrs?: Record<string, string | number>,
) {
this.queues = [
meteredList({ priority: CO_VALUE_PRIORITY.HIGH, ...attrs }),
meteredList({ priority: CO_VALUE_PRIORITY.MEDIUM, ...attrs }),
meteredList({ priority: CO_VALUE_PRIORITY.LOW, ...attrs }),
];
}
private getQueue(priority: CoValuePriority) {
return this.queues[PRIORITY_TO_QUEUE_INDEX[priority]];
}
public push(msg: SyncMessage) { public push(msg: SyncMessage) {
const { promise, resolve, reject } = promiseWithResolvers<void>(); const { promise, resolve, reject } = promiseWithResolvers<void>();
@@ -119,24 +171,12 @@ export class PriorityBasedMessageQueue {
this.getQueue(priority).push(entry); this.getQueue(priority).push(entry);
this.queueSizeCounter.add(1, {
priority,
});
return promise; return promise;
} }
public pull() { public pull() {
const priority = this.queues.findIndex((queue) => queue.length > 0); const priority = this.queues.findIndex((queue) => queue.length > 0);
if (priority === -1) {
return;
}
this.queueSizeCounter.add(-1, {
priority,
});
return this.queues[priority]?.shift(); return this.queues[priority]?.shift();
} }
} }

View File

@@ -7,7 +7,7 @@ import { type CoValueHeader } from "./coValueCore.js";
* The priority value is handled as weight in the weighed round robin algorithm * The priority value is handled as weight in the weighed round robin algorithm
* used to determine the order in which messages are sent. * used to determine the order in which messages are sent.
* *
* Follows the HTTP urgency range and order: * Loosely follows the HTTP urgency range and order, but limited to 3 values:
* - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency * - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency
*/ */
export const CO_VALUE_PRIORITY = { export const CO_VALUE_PRIORITY = {
@@ -16,7 +16,7 @@ export const CO_VALUE_PRIORITY = {
LOW: 6, LOW: 6,
} as const; } as const;
export type CoValuePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; export type CoValuePriority = 0 | 3 | 6;
export function getPriorityFromHeader( export function getPriorityFromHeader(
header: CoValueHeader | undefined | boolean, header: CoValueHeader | undefined | boolean,

View File

@@ -7,9 +7,9 @@ import {
tearDownTestMetricReader, tearDownTestMetricReader,
} from "./testUtils.js"; } from "./testUtils.js";
function setup() { function setup(attrs?: Record<string, string | number>) {
const metricReader = createTestMetricReader(); const metricReader = createTestMetricReader();
const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM); const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM, attrs);
return { queue, metricReader }; return { queue, metricReader };
} }
@@ -18,10 +18,133 @@ describe("PriorityBasedMessageQueue", () => {
tearDownTestMetricReader(); tearDownTestMetricReader();
}); });
describe("meteredQueue", () => {
test("should corretly count pushes", async () => {
const { queue, metricReader } = setup();
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(0);
void queue.push(message);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(1);
void queue.push(message);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(2);
});
test("should corretly count pulls", async () => {
const { queue, metricReader } = setup();
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(0);
void queue.push(message);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(0);
void queue.pull();
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(1);
// We only have one item in the queue, so this should not change the metric value
void queue.pull();
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
}),
).toBe(1);
});
test("should corretly set custom attributes to the metrics", async () => {
const { queue, metricReader } = setup({ role: "server" });
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "server",
}),
).toBe(0);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "client",
}),
).toBeUndefined();
void queue.push(message);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "server",
}),
).toBe(1);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "server",
}),
).toBe(0);
void queue.pull();
expect(
await metricReader.getMetricValue("jazz.messagequeue.pushed", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "server",
}),
).toBe(1);
expect(
await metricReader.getMetricValue("jazz.messagequeue.pulled", {
priority: CO_VALUE_PRIORITY.MEDIUM,
role: "server",
}),
).toBe(1);
});
});
test("should initialize with correct properties", () => { test("should initialize with correct properties", () => {
const { queue } = setup(); const { queue } = setup();
expect(queue["defaultPriority"]).toBe(CO_VALUE_PRIORITY.MEDIUM); expect(queue["defaultPriority"]).toBe(CO_VALUE_PRIORITY.MEDIUM);
expect(queue["queues"].length).toBe(8); expect(queue["queues"].length).toBe(3);
expect(queue["queues"].every((q) => !q.length)).toBe(true); expect(queue["queues"].every((q) => !q.length)).toBe(true);
}); });
@@ -52,7 +175,7 @@ describe("PriorityBasedMessageQueue", () => {
}); });
test("should pull messages in priority order", async () => { test("should pull messages in priority order", async () => {
const { queue, metricReader } = setup(); const { queue } = setup();
const lowPriorityMsg: SyncMessage = { const lowPriorityMsg: SyncMessage = {
action: "content", action: "content",
id: "co_zlow", id: "co_zlow",
@@ -73,42 +196,12 @@ describe("PriorityBasedMessageQueue", () => {
}; };
void queue.push(lowPriorityMsg); void queue.push(lowPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: lowPriorityMsg.priority,
}),
).toBe(1);
void queue.push(mediumPriorityMsg); void queue.push(mediumPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: mediumPriorityMsg.priority,
}),
).toBe(1);
void queue.push(highPriorityMsg); void queue.push(highPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: highPriorityMsg.priority,
}),
).toBe(1);
expect(queue.pull()?.msg).toEqual(highPriorityMsg); expect(queue.pull()?.msg).toEqual(highPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: highPriorityMsg.priority,
}),
).toBe(0);
expect(queue.pull()?.msg).toEqual(mediumPriorityMsg); expect(queue.pull()?.msg).toEqual(mediumPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: mediumPriorityMsg.priority,
}),
).toBe(0);
expect(queue.pull()?.msg).toEqual(lowPriorityMsg); expect(queue.pull()?.msg).toEqual(lowPriorityMsg);
expect(
await metricReader.getMetricValue("jazz.messagequeue.size", {
priority: lowPriorityMsg.priority,
}),
).toBe(0);
}); });
test("should return undefined when pulling from empty queue", () => { test("should return undefined when pulling from empty queue", () => {

View File

@@ -1,5 +1,11 @@
# jazz-browser # jazz-browser
## 0.13.1
### Patch Changes
- 63a7aa0: Add exports for clerk and crypto to make import shortcuts work everywhere
## 0.13.0 ## 0.13.0
### Minor Changes ### Minor Changes

2
packages/jazz-expo/auth/clerk.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// Re-export the Clerk provider from the dist folder to make the `jazz-expo/auth/clerk` import work everywhere
export * from "../dist/auth/clerk";

View File

@@ -0,0 +1,2 @@
// Re-export the Clerk provider from the dist folder to make the `jazz-expo/auth/clerk` import work everywhere
export * from "../dist/auth/clerk";

2
packages/jazz-expo/crypto/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// Re-export the crypto module from the dist folder to make the `jazz-expo/crypto` import work everywhere
export * from "jazz-react-native-core/dist/crypto";

View File

@@ -0,0 +1,2 @@
// Re-export the crypto module from the dist folder to make the `jazz-expo/crypto` import work everywhere
export * from "jazz-react-native-core/dist/crypto";

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-expo", "name": "jazz-expo",
"version": "0.13.0", "version": "0.13.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -22,6 +22,11 @@
"types": "./dist/auth/clerk/index.d.ts", "types": "./dist/auth/clerk/index.d.ts",
"default": "./dist/auth/clerk/index.js" "default": "./dist/auth/clerk/index.js"
}, },
"./crypto": {
"react-native": "./crypto/index.js",
"types": "./crypto/index.d.ts",
"default": "./crypto/index.js"
},
"./testing": { "./testing": {
"react-native": "./dist/testing.js", "react-native": "./dist/testing.js",
"types": "./dist/testing.d.ts", "types": "./dist/testing.d.ts",

View File

@@ -22,7 +22,6 @@ function JazzProvider({ children }: { children: React.ReactNode }) {
return ( return (
<JazzProviderWithClerk <JazzProviderWithClerk
clerk={clerk} clerk={clerk}
storage="sqlite"
sync={{ sync={{
peer: "wss://cloud.jazz.tools/?key=chat-rn-expo-clerk-example-jazz@garden.co", peer: "wss://cloud.jazz.tools/?key=chat-rn-expo-clerk-example-jazz@garden.co",
}} }}

View File

@@ -1,5 +1,11 @@
# jazz-browser # jazz-browser
## 0.13.1
### Patch Changes
- 63a7aa0: Add exports for clerk and crypto to make import shortcuts work everywhere
## 0.13.0 ## 0.13.0
### Minor Changes ### Minor Changes

View File

@@ -0,0 +1,2 @@
// Re-export the crypto module from the dist folder to make the `jazz-react-native/crypto` import work everywhere
export * from "jazz-react-native-core/dist/crypto";

View File

@@ -0,0 +1,2 @@
// Re-export the crypto module from the dist folder to make the `jazz-react-native/crypto` import work everywhere
export * from "jazz-react-native-core/dist/crypto";

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-react-native", "name": "jazz-react-native",
"version": "0.13.0", "version": "0.13.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -12,6 +12,11 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "default": "./dist/index.js"
}, },
"./crypto": {
"react-native": "./crypto/index.js",
"types": "./crypto/index.d.ts",
"default": "./crypto/index.js"
},
"./testing": { "./testing": {
"react-native": "./dist/testing.js", "react-native": "./dist/testing.js",
"types": "./dist/testing.d.ts", "types": "./dist/testing.d.ts",