Compare commits

..

43 Commits

Author SHA1 Message Date
Guido D'Orsi
4c2e60ab51 Merge pull request #2776 from garden-co/changeset-release/main
Version Packages
2025-08-20 13:25:30 +02:00
github-actions[bot]
1830171930 Version Packages 2025-08-20 10:59:08 +00:00
Guido D'Orsi
6574090402 Merge pull request #2777 from garden-co/fix/load-as-upsertUnique
Explicit loadAs in upsertUnique to use it without loaded context
2025-08-20 12:55:18 +02:00
Guido D'Orsi
a6f65472a7 Merge pull request #2779 from ccssmnn/add-fallback-components
Add Fallback properties to JazzProvider and ClerkProvider
2025-08-20 12:54:16 +02:00
Guido D'Orsi
109952aa6a Merge pull request #2781 from garden-co/gio/setup-codeowners
chore: setup codeowners
2025-08-20 12:50:38 +02:00
Giordano Ricci
556bdf977b more tweaking 2025-08-20 11:41:31 +01:00
Giordano Ricci
0213b4e5b9 add ui 2025-08-20 11:31:54 +01:00
Giordano Ricci
01c195756c add docs 2025-08-20 11:27:17 +01:00
Giordano Ricci
9c69917d24 add @garden-co/framework as owner of all packages 2025-08-20 11:03:18 +01:00
Giordano Ricci
b4d68f0a32 wip: setup codeowners 2025-08-20 10:59:36 +01:00
Guido D'Orsi
fbc3839777 chore: simplify the fallback on the react provider 2025-08-20 11:55:45 +02:00
Guido D'Orsi
296464b282 Merge pull request #2753 from garden-co/GCO-642-clarify-docs-adding-group-members-by-id
Closes #GCO-642 - Clarify docs for adding group members by id
2025-08-20 11:50:03 +02:00
Carl Assmann
c463654970 no need to add fallback to clerk, bc it extends and forwards jazzproviderprops 2025-08-19 23:23:19 +02:00
Carl Assmann
debc052bdc add fallback to clerk example 2025-08-19 23:12:12 +02:00
Carl Assmann
3c7846153d add fallback properties to JazzProvider and ClerkProvider 2025-08-19 23:00:06 +02:00
Meg Culotta
e410fedda7 Merge pull request #2736 from garden-co/#GCO-691-Adds-node-20-troubleshooting-docs
Closes GCO-691, GCO-731 - Add setup troubleshooting page
2025-08-19 14:40:47 -05:00
Matteo Manchi
52ea0c7a9b fix(jazz-tools/tools): explicit loadAs in upsertUnique to use it without loaded context 2025-08-19 18:21:31 +02:00
Guido D'Orsi
b91c93caed Merge pull request #2769 from garden-co/justin-gco-741-skip-session-transaction-verification-in-core-node
Ref GCO-741: Add option for disabling transaction verification to SyncManager
2025-08-19 18:03:02 +02:00
Justin Rosenthal
7586c3bac5 Add changeset 2025-08-19 08:52:44 -07:00
Justin Rosenthal
9b96cd4a65 Craft an invalid transaction in unit test 2025-08-19 08:50:43 -07:00
Justin Rosenthal
2e66ea8e56 Revert signatures to positional args 2025-08-19 08:32:38 -07:00
Guido D'Orsi
336cc1f0fe Merge pull request #2773 from garden-co/changeset-release/main
Version Packages
2025-08-19 16:27:36 +02:00
github-actions[bot]
cc2ca5c23c Version Packages 2025-08-19 14:26:38 +00:00
Guido D'Orsi
3664385113 Merge pull request #2775 from garden-co/fix/comap-coprofile-validate-input
fix: prevent passing CoValue schemas to `co.map` and `co.profile`
2025-08-19 16:22:45 +02:00
Guido D'Orsi
2b2ecdaf3d Merge pull request #2771 from garden-co/feat/rich-text-prosemirror-fix
fix(prosemirror): fix RangeError triggered when creating invalid HTML
2025-08-19 16:21:39 +02:00
Guido D'Orsi
6dbb05320a fix(prosemirror): fix RangeError triggered when creating invalid HTML 2025-08-19 16:15:12 +02:00
Margaret Culotta
0160a188fa Remove strict casting from example 2025-08-19 09:06:58 -05:00
NicoR
ac3e694f4e fix: prevent passing CoValue schemas to co.map and co.profile 2025-08-19 11:05:54 -03:00
Margaret Culotta
a7dca75955 Clean up unnecessary content per PR feedback 2025-08-19 08:29:24 -05:00
Guido D'Orsi
143156cd6a Merge pull request #2772 from garden-co/gio/add-missing-export
fix: add missing export
2025-08-19 14:31:57 +02:00
Giordano Ricci
1a182f07de add changeset 2025-08-19 13:30:30 +01:00
Giordano Ricci
7e7e7ebb51 fix: add missing export 2025-08-19 13:28:45 +01:00
Guido D'Orsi
0966a90f3d Merge pull request #2768 from garden-co/changeset-release/main
Version Packages
2025-08-19 08:44:03 +02:00
Justin Rosenthal
76f142b70d Add ability to disable verification in SyncManager 2025-08-18 14:52:58 -07:00
github-actions[bot]
cd2f0846db Version Packages 2025-08-18 20:33:16 +00:00
Guido D'Orsi
c2e411d056 Merge pull request #2759 from 0x100101/feat/sync-server-host-opt
Add host option to the jazz-run sync command
2025-08-18 22:31:01 +02:00
Justin Rosenthal
0167153da2 Use named parameters in tryAddTransactions() signatures 2025-08-18 13:19:26 -07:00
0x100101
feaa69ebdd Add patch file 2025-08-18 10:04:04 -05:00
0x100101
d5fa172b17 Update docs 2025-08-17 20:46:12 -05:00
0x100101
96de15593b Add and update tests. Tweak sync server return. 2025-08-17 20:32:22 -05:00
0x100101
5ba03ebc70 Add host option to startSyncServerCommand command 2025-08-17 16:10:04 -05:00
Margaret Culotta
d8ae47c4d1 Clarify docs for adding group members by id 2025-08-15 15:01:38 -05:00
Margaret Culotta
92c0048984 add setup troubleshooting page 2025-08-14 10:32:22 -05:00
62 changed files with 846 additions and 113 deletions

8
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,8 @@
./packages @garden-co/framework
./tests @garden-co/framework
./packages/quint-ui @garden-co/ui
./homepage @garden-co/ui
./homepage/homepage/content/docs @garden-co/docs
./starters @garden-co/docs
./examples @garden-co/docs @garden-co/ui

View File

@@ -1,5 +1,27 @@
# passkey-svelte # passkey-svelte
## 0.0.122
### Patch Changes
- Updated dependencies [52ea0c7]
- jazz-tools@0.17.9
## 0.0.121
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
## 0.0.120
### Patch Changes
- jazz-tools@0.17.7
## 0.0.119 ## 0.0.119
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "chat-svelte", "name": "chat-svelte",
"version": "0.0.119", "version": "0.0.122",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -9,8 +9,10 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root">
<p>Loading...</p>
</div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -24,6 +24,7 @@ function JazzProvider({ children }: { children: ReactNode }) {
sync={{ sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`, peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}} }}
fallback={<p>Loading...</p>}
> >
{children} {children}
</JazzReactProviderWithClerk> </JazzReactProviderWithClerk>

View File

@@ -25,6 +25,11 @@ export const docNavigationItems = [
excludeFromNavigation: true, excludeFromNavigation: true,
}, },
{ name: "FAQs", href: "/docs/faq", done: 100 }, { name: "FAQs", href: "/docs/faq", done: 100 },
{
name: "Troubleshooting",
href: "/docs/troubleshooting",
done: 100,
},
], ],
}, },
{ {

View File

@@ -49,18 +49,20 @@ if (bob) {
``` ```
</CodeGroup> </CodeGroup>
**Note:** if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID<Account>` first: **Note:**
- Both `Account.load(id)` and `co.account().load(id)` do the same thing — they load an account from its ID.
<CodeGroup> <CodeGroup>
```tsx twoslash ```tsx twoslash
const bobsID = "co_z123"; const bobsID = "co_z123";
import { Group } from "jazz-tools";
const group = Group.create(); const group = Group.create();
// ---cut--- // ---cut---
import { co, Group } from "jazz-tools"; import { ID, Account, co } from "jazz-tools";
const bob = await co.account().load(bobsID); const bob = await Account.load(bobsID);
// Or: const bob = await co.account().load(bobsID);
if (bob) { if (bob) {
group.addMember(bob, "writer"); group.addMember(bob, "writer");

View File

@@ -25,7 +25,10 @@ You can use [`create-jazz-app`](/docs/tools/create-jazz-app) to create a new Jaz
``` ```
</CodeGroup> </CodeGroup>
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert> <Alert variant="info" className="mt-4 flex gap-2 items-center">
Requires at least Node.js v20.
See our [Troubleshooting Guide](https://jazz.tools/docs/troubleshooting) for quick fixes.
</Alert>
{/* <ContentByFramework framework="react"> {/* <ContentByFramework framework="react">
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app. Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.

View File

@@ -53,6 +53,7 @@ npm i -S jazz-tools
<Alert variant="info" className="mt-4" title="Note"> <Alert variant="info" className="mt-4" title="Note">
- Requires at least Node.js v20. - Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`. - Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
See our [Troubleshooting Guide](https://jazz.tools/docs/troubleshooting) for quick fixes.
</Alert> </Alert>
#### Fix incompatible dependencies #### Fix incompatible dependencies

View File

@@ -51,7 +51,6 @@ npm i -S jazz-tools
</CodeGroup> </CodeGroup>
<Alert variant="info" className="mt-4" title="Note"> <Alert variant="info" className="mt-4" title="Note">
- Requires at least Node.js v20.
- Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`. - Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
</Alert> </Alert>

View File

@@ -27,8 +27,6 @@ pnpm install jazz-tools
``` ```
</CodeGroup> </CodeGroup>
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
## Write your schema ## Write your schema
Define your data schema using [CoValues](/docs/schemas/covalues) from `jazz-tools`. Define your data schema using [CoValues](/docs/schemas/covalues) from `jazz-tools`.

View File

@@ -19,8 +19,6 @@ pnpm install jazz-tools
``` ```
</CodeGroup> </CodeGroup>
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>
## Write your schema ## Write your schema
See the [schema docs](/docs/schemas/covalues) for more information. See the [schema docs](/docs/schemas/covalues) for more information.

View File

@@ -1,4 +1,5 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx' import { ContentByFramework, CodeGroup } from '@/components/forMdx'
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
export const metadata = { export const metadata = {
description: "Learn how to sync and persist your data using Jazz Cloud, or run your own sync server." description: "Learn how to sync and persist your data using Jazz Cloud, or run your own sync server."
@@ -44,8 +45,14 @@ And then use `ws://localhost:4200` as the sync server URL.
You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS. You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS.
In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL. In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL.
<Alert variant="info" className="mt-4 flex gap-2 items-center">
Requires at least Node.js v20.
See our [Troubleshooting Guide](https://jazz.tools/docs/troubleshooting) for quick fixes.
</Alert>
### Command line options: ### Command line options:
- `--host` / `-h` - the host to run the sync server on. Defaults to 127.0.0.1.
- `--port` / `-p` - the port to run the sync server on. Defaults to 4200. - `--port` / `-p` - the port to run the sync server on. Defaults to 4200.
- `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default. - `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default.
- `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`. - `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`.

View File

@@ -0,0 +1,133 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
export const metadata = {
title: "Setup troubleshooting",
description: "A few reported setup hiccups and how to fix them."
};
# Setup troubleshooting
A few reported setup hiccups and how to fix them.
---
## Node.js version requirements
Jazz requires **Node.js v20 or later** due to native module dependencies.
Check your version:
<CodeGroup>
```sh
node -v
```
</CodeGroup>
If youre on Node 18 or earlier, upgrade via nvm:
<CodeGroup>
```sh
nvm install 20
nvm use 20
```
</CodeGroup>
---
## npx jazz-run: command not found
If, when running:
<CodeGroup>
```sh
npx jazz-run sync
```
</CodeGroup>
you encounter:
<CodeGroup>
```sh
sh: jazz-run: command not found
```
</CodeGroup>
This is often due to an npx cache quirk. (For most apps using Jazz)
1. Clear your npx cache:
<CodeGroup>
```sh
npx clear-npx-cache
```
</CodeGroup>
2. Rerun the command:
<CodeGroup>
```sh
npx jazz-run sync
```
</CodeGroup>
---
### Node 18 workaround (rebuilding the native module)
If you cant upgrade to Node 20+, you can rebuild the native `better-sqlite3` module for your architecture.
1. Install `jazz-run` locally in your project:
<CodeGroup>
```sh
pnpm add -D jazz-run
```
</CodeGroup>
2. Find the installed version of better-sqlite3 inside node_modules.
It should look like this:
<CodeGroup>
```sh
./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3
```
</CodeGroup>
Replace `{version}` with your installed version and run:
<CodeGroup>
```sh
# Navigate to the installed module and rebuild
pushd ./node_modules/.pnpm/better-sqlite3{version}/node_modules/better-sqlite3
&& pnpm install
&& popd
```
</CodeGroup>
If you get ModuleNotFoundError: No module named 'distutils':
Linux:
<CodeGroup>
```sh
pip install --upgrade setuptools
```
</CodeGroup>
macOS:
<CodeGroup>
```sh
brew install python-setuptools
```
</CodeGroup>
<p><i>Workaround originally shared by @aheissenberger on Jun 24, 2025.</i></p>
---
### Still having trouble?
If none of the above fixes work:
Make sure dependencies installed without errors (`pnpm install`).
Double-check your `node -v` output matches the required version.
Open an issue on GitHub with:
- Your OS and version
- Node.js version
- Steps you ran and full error output
We're always happy to help! If you're stuck, reachout via [Discord](https://discord.gg/utDMjHYg42)

View File

@@ -26,3 +26,4 @@ export const navigationItems: NavItemProps[] = [
title: "Status", title: "Status",
}, },
]; ];

View File

@@ -1,5 +1,24 @@
# cojson-storage-indexeddb # cojson-storage-indexeddb
## 0.17.9
### Patch Changes
- Updated dependencies [7586c3b]
- cojson@0.17.9
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,5 +1,24 @@
# cojson-storage-sqlite # cojson-storage-sqlite
## 0.17.9
### Patch Changes
- Updated dependencies [7586c3b]
- cojson@0.17.9
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "cojson-storage-sqlite", "name": "cojson-storage-sqlite",
"type": "module", "type": "module",
"version": "0.17.6", "version": "0.17.9",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"license": "MIT", "license": "MIT",

View File

@@ -1,5 +1,24 @@
# cojson-transport-nodejs-ws # cojson-transport-nodejs-ws
## 0.17.9
### Patch Changes
- Updated dependencies [7586c3b]
- cojson@0.17.9
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

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

View File

@@ -1,5 +1,15 @@
# cojson # cojson
## 0.17.9
### Patch Changes
- 7586c3b: Adds disableTransactionVerification() method to SyncManager
## 0.17.8
## 0.17.7
## 0.17.6 ## 0.17.6
## 0.17.5 ## 0.17.5

View File

@@ -25,7 +25,7 @@
}, },
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.17.6", "version": "0.17.9",
"devDependencies": { "devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-metrics": "^2.0.0",
"libsql": "^0.5.13", "libsql": "^0.5.13",

View File

@@ -132,6 +132,11 @@ export class SyncManager {
peers: { [key: PeerID]: PeerState } = {}; peers: { [key: PeerID]: PeerState } = {};
local: LocalNode; local: LocalNode;
// When true, transactions will not be verified.
// This is useful when syncing only for storage purposes, with the expectation that
// the transactions have already been verified by the [trusted] peer that sent them.
private skipVerify: boolean = false;
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", { peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
description: "Amount of connected peers", description: "Amount of connected peers",
valueType: ValueType.INT, valueType: ValueType.INT,
@@ -154,6 +159,10 @@ export class SyncManager {
syncState: SyncStateManager; syncState: SyncStateManager;
disableTransactionVerification() {
this.skipVerify = true;
}
peersInPriorityOrder(): PeerState[] { peersInPriorityOrder(): PeerState[] {
return Object.values(this.peers).sort((a, b) => { return Object.values(this.peers).sort((a, b) => {
const aPriority = a.priority || 0; const aPriority = a.priority || 0;
@@ -637,6 +646,7 @@ export class SyncManager {
undefined, undefined,
newContentForSession.lastSignature, newContentForSession.lastSignature,
"immediate", "immediate",
this.skipVerify,
); );
if (result.isErr()) { if (result.isErr()) {

View File

@@ -20,11 +20,13 @@ import {
createTestNode, createTestNode,
loadCoValueOrFail, loadCoValueOrFail,
nodeWithRandomAgentAndSessionID, nodeWithRandomAgentAndSessionID,
randomAgentAndSessionID,
setupTestAccount, setupTestAccount,
setupTestNode, setupTestNode,
tearDownTestMetricReader, tearDownTestMetricReader,
waitFor, waitFor,
} from "./testUtils.js"; } from "./testUtils.js";
import { stableStringify } from "../jsonStringify.js";
// We want to simulate a real world communication that happens asynchronously // We want to simulate a real world communication that happens asynchronously
TEST_NODE_CONFIG.withAsyncPeers = true; TEST_NODE_CONFIG.withAsyncPeers = true;
@@ -160,6 +162,36 @@ test("should delete the peer state when the peer closes if persistent is false",
expect(syncManager.peers[peer.id]).toBeUndefined(); expect(syncManager.peers[peer.id]).toBeUndefined();
}); });
test("should not verify transactions when SyncManager has verification disabled", async () => {
jazzCloud.node.syncManager.disableTransactionVerification();
const [agent] = randomAgentAndSessionID();
const client = await setupTestAccount({ connected: true });
const group = client.node.createGroup();
const map = group.createMap();
map.core.tryAddTransactions(
client.node.currentSessionID,
[
{
privacy: "trusting",
changes: stableStringify([{ op: "set", key: "hello", value: "world" }]),
madeAt: Date.now(),
},
],
undefined,
Crypto.sign(agent.currentSignerSecret(), "hash_z12345678"),
"immediate",
true,
);
await map.core.waitForSync();
const loadedMap = await loadCoValueOrFail(jazzCloud.node, map.id);
expect(loadedMap.get("hello")).toEqual("world");
});
describe("sync - extra tests", () => { describe("sync - extra tests", () => {
test("Node handles disconnection and reconnection of a peer gracefully", async () => { test("Node handles disconnection and reconnection of a peer gracefully", async () => {
// Create two nodes // Create two nodes

View File

@@ -1,5 +1,31 @@
# jazz-react # jazz-react
## 0.17.9
### Patch Changes
- Updated dependencies [52ea0c7]
- Updated dependencies [7586c3b]
- jazz-tools@0.17.9
- cojson@0.17.9
## 0.17.8
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-tools@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "community-jazz-vue", "name": "community-jazz-vue",
"version": "0.17.6", "version": "0.17.9",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,34 @@
# jazz-auth-betterauth # jazz-auth-betterauth
## 0.17.9
### Patch Changes
- Updated dependencies [52ea0c7]
- Updated dependencies [7586c3b]
- jazz-tools@0.17.9
- cojson@0.17.9
- jazz-betterauth-client-plugin@0.17.9
## 0.17.8
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
- jazz-betterauth-client-plugin@0.17.8
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-betterauth-client-plugin@0.17.7
- jazz-tools@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-auth-betterauth", "name": "jazz-auth-betterauth",
"version": "0.17.6", "version": "0.17.9",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,23 @@
# jazz-betterauth-client-plugin # jazz-betterauth-client-plugin
## 0.17.9
### Patch Changes
- jazz-betterauth-server-plugin@0.17.9
## 0.17.8
### Patch Changes
- jazz-betterauth-server-plugin@0.17.8
## 0.17.7
### Patch Changes
- jazz-betterauth-server-plugin@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-betterauth-client-plugin", "name": "jazz-betterauth-client-plugin",
"version": "0.17.6", "version": "0.17.9",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,31 @@
# jazz-betterauth-server-plugin # jazz-betterauth-server-plugin
## 0.17.9
### Patch Changes
- Updated dependencies [52ea0c7]
- Updated dependencies [7586c3b]
- jazz-tools@0.17.9
- cojson@0.17.9
## 0.17.8
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-tools@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-betterauth-server-plugin", "name": "jazz-betterauth-server-plugin",
"version": "0.17.6", "version": "0.17.9",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,5 +1,37 @@
# jazz-react-auth-betterauth # jazz-react-auth-betterauth
## 0.17.9
### Patch Changes
- Updated dependencies [52ea0c7]
- Updated dependencies [7586c3b]
- jazz-tools@0.17.9
- cojson@0.17.9
- jazz-auth-betterauth@0.17.9
- jazz-betterauth-client-plugin@0.17.9
## 0.17.8
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
- jazz-auth-betterauth@0.17.8
- jazz-betterauth-client-plugin@0.17.8
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-auth-betterauth@0.17.7
- jazz-betterauth-client-plugin@0.17.7
- jazz-tools@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "jazz-react-auth-betterauth", "name": "jazz-react-auth-betterauth",
"version": "0.17.6", "version": "0.17.9",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.tsx", "types": "src/index.tsx",

View File

@@ -1,5 +1,38 @@
# jazz-run # jazz-run
## 0.17.9
### Patch Changes
- Updated dependencies [52ea0c7]
- Updated dependencies [7586c3b]
- jazz-tools@0.17.9
- cojson@0.17.9
- cojson-storage-sqlite@0.17.9
- cojson-transport-ws@0.17.9
## 0.17.8
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
- cojson@0.17.8
- cojson-storage-sqlite@0.17.8
- cojson-transport-ws@0.17.8
## 0.17.7
### Patch Changes
- feaa69e: Add host option to the jazz-run sync command
- cojson@0.17.7
- cojson-storage-sqlite@0.17.7
- cojson-transport-ws@0.17.7
- jazz-tools@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js", "bin": "./dist/index.js",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.17.6", "version": "0.17.9",
"exports": { "exports": {
"./startSyncServer": { "./startSyncServer": {
"types": "./dist/startSyncServer.d.ts", "types": "./dist/startSyncServer.d.ts",
@@ -28,11 +28,11 @@
"@effect/printer-ansi": "^0.34.5", "@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1", "@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5", "@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.17.6", "cojson": "workspace:0.17.9",
"cojson-storage-sqlite": "workspace:0.17.6", "cojson-storage-sqlite": "workspace:0.17.9",
"cojson-transport-ws": "workspace:0.17.6", "cojson-transport-ws": "workspace:0.17.9",
"effect": "^3.6.5", "effect": "^3.6.5",
"jazz-tools": "workspace:0.17.6", "jazz-tools": "workspace:0.17.9",
"ws": "^8.14.2" "ws": "^8.14.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,6 @@
export const serverDefaults = {
host: "127.0.0.1",
port: 4200,
inMemory: false,
db: "sync-db/storage.db",
};

View File

@@ -5,6 +5,7 @@ import { NodeContext, NodeRuntime } from "@effect/platform-node";
import { Console, Effect } from "effect"; import { Console, Effect } from "effect";
import { createWorkerAccount } from "./createWorkerAccount.js"; import { createWorkerAccount } from "./createWorkerAccount.js";
import { startSyncServer } from "./startSyncServer.js"; import { startSyncServer } from "./startSyncServer.js";
import { serverDefaults } from "./config.js";
const jazzTools = Command.make("jazz-tools"); const jazzTools = Command.make("jazz-tools");
@@ -39,36 +40,63 @@ const accountCommand = Command.make("account").pipe(
Command.withSubcommands([createAccountCommand]), Command.withSubcommands([createAccountCommand]),
); );
const hostOption = Options.text("host")
.pipe(Options.withAlias("h"))
.pipe(
Options.withDescription(
`The host to listen on. Default is ${serverDefaults.host}`,
),
)
.pipe(Options.withDefault(serverDefaults.host));
const portOption = Options.text("port") const portOption = Options.text("port")
.pipe(Options.withAlias("p")) .pipe(Options.withAlias("p"))
.pipe( .pipe(
Options.withDescription( Options.withDescription(
"Select a different port for the WebSocket server. Default is 4200", `Select a different port for the WebSocket server. Default is ${serverDefaults.port}`,
), ),
) )
.pipe(Options.withDefault("4200")); .pipe(Options.withDefault(serverDefaults.port.toString()));
const inMemoryOption = Options.boolean("in-memory").pipe( const inMemoryOption = Options.boolean("in-memory").pipe(
Options.withDescription("Use an in-memory storage instead of file-based"), Options.withDescription("Use an in-memory storage instead of file-based."),
); );
const dbOption = Options.file("db") const dbOption = Options.file("db")
.pipe( .pipe(
Options.withDescription( Options.withDescription(
"The path to the file where to store the data. Default is 'sync-db/storage.db'", `The path to the file where to store the data. Default is '${serverDefaults.db}'`,
), ),
) )
.pipe(Options.withDefault("sync-db/storage.db")); .pipe(Options.withDefault(serverDefaults.db));
const startSyncServerCommand = Command.make( const startSyncServerCommand = Command.make(
"sync", "sync",
{ port: portOption, inMemory: inMemoryOption, db: dbOption }, {
({ port, inMemory, db }) => { host: hostOption,
port: portOption,
inMemory: inMemoryOption,
db: dbOption,
},
({ host, port, inMemory, db }) => {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* Effect.promise(() => startSyncServer({ port, inMemory, db })); const server = yield* Effect.promise(() =>
startSyncServer({ host, port, inMemory, db }),
);
const serverAddress = server.address();
if (!serverAddress) {
return yield* Effect.fail(new Error("Failed to start sync server."));
}
const socketAddress =
typeof serverAddress === "object"
? `${serverAddress.address}:${serverAddress.port}`
: serverAddress;
yield* Console.log( yield* Console.log(
`COJSON sync server listening on ws://127.0.0.1:${port}`, `COJSON sync server listening on ws://${socketAddress}`,
); );
// Keep the server up // Keep the server up

View File

@@ -1,4 +1,4 @@
import { createServer, type Server } from "node:http"; import { createServer } from "node:http";
import { mkdir } from "node:fs/promises"; import { mkdir } from "node:fs/promises";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { LocalNode } from "cojson"; import { LocalNode } from "cojson";
@@ -6,16 +6,19 @@ import { getBetterSqliteStorage } from "cojson-storage-sqlite";
import { createWebSocketPeer } from "cojson-transport-ws"; import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto"; import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { type SyncServer } from "./types.js";
export const startSyncServer = async ({ export const startSyncServer = async ({
host,
port, port,
inMemory, inMemory,
db, db,
}: { }: {
host: string | undefined;
port: string | undefined; port: string | undefined;
inMemory: boolean; inMemory: boolean;
db: string; db: string;
}) => { }): Promise<SyncServer> => {
const crypto = await WasmCrypto.create(); const crypto = await WasmCrypto.create();
const server = createServer((req, res) => { const server = createServer((req, res) => {
@@ -94,8 +97,6 @@ export const startSyncServer = async ({
localNode.gracefulShutdown(); localNode.gracefulShutdown();
}); });
server.listen(port ? parseInt(port) : undefined);
const _close = server.close; const _close = server.close;
server.close = () => { server.close = () => {
@@ -106,5 +107,11 @@ export const startSyncServer = async ({
Object.defineProperty(server, "localNode", { value: localNode }); Object.defineProperty(server, "localNode", { value: localNode });
return server as Server & { localNode: LocalNode }; server.listen(port ? parseInt(port) : undefined, host);
return new Promise((resolve) => {
server.once("listening", () => {
resolve(server as SyncServer);
});
});
}; };

View File

@@ -5,11 +5,13 @@ import { describe, expect, it, onTestFinished } from "vitest";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { createWorkerAccount } from "../createWorkerAccount.js"; import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js"; import { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
describe("createWorkerAccount - integration tests", () => { describe("createWorkerAccount - integration tests", () => {
it("should create a worker account using the local sync server", async () => { it("should create a worker account using the local sync server", async () => {
// Pass port: undefined to let the server choose a random port // Pass port: undefined to let the server choose a random port
const server = await startSyncServer({ const server = await startSyncServer({
host: serverDefaults.host,
port: undefined, port: undefined,
inMemory: true, inMemory: true,
db: "", db: "",

View File

@@ -1,24 +1,29 @@
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path"; import { join } from "path";
import { LocalNode } from "cojson";
import { co, z } from "jazz-tools"; import { co, z } from "jazz-tools";
import { startWorker } from "jazz-tools/worker"; import { startWorker } from "jazz-tools/worker";
import { describe, expect, test } from "vitest"; import { describe, expect, test, afterAll } from "vitest";
import { createWorkerAccount } from "../createWorkerAccount.js"; import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js"; import { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
import { unlinkSync } from "node:fs";
const TestMap = co.map({ const TestMap = co.map({
value: z.string(), value: z.string(),
}); });
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
afterAll(() => {
unlinkSync(dbPath);
});
describe("startSyncServer", () => { describe("startSyncServer", () => {
test("persists values in storage and loads them after restart", async () => { test("persists values in storage and loads them after restart", async () => {
// Create a temporary database file
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
// Start first server instance // Start first server instance
const server1 = await startSyncServer({ const server1 = await startSyncServer({
host: serverDefaults.host,
port: "0", // Random available port port: "0", // Random available port
inMemory: false, inMemory: false,
db: dbPath, db: dbPath,
@@ -48,6 +53,7 @@ describe("startSyncServer", () => {
// Start second server instance with same DB // Start second server instance with same DB
const server2 = await startSyncServer({ const server2 = await startSyncServer({
host: serverDefaults.host,
port: "0", port: "0",
inMemory: false, inMemory: false,
db: dbPath, db: dbPath,
@@ -74,4 +80,21 @@ describe("startSyncServer", () => {
await worker2.done(); await worker2.done();
server2.close(); server2.close();
}); });
test("starts a sync server with a specific host and port", async () => {
const server = await startSyncServer({
host: "0.0.0.0",
port: "4900",
inMemory: false,
db: dbPath,
});
expect(server.address()).toEqual({
address: "0.0.0.0",
port: 4900,
family: "IPv4",
});
server.close();
});
}); });

View File

@@ -18,6 +18,7 @@ import { afterAll, describe, expect, onTestFinished, test } from "vitest";
import { createWorkerAccount } from "../createWorkerAccount.js"; import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js"; import { startSyncServer } from "../startSyncServer.js";
import { waitFor } from "./utils.js"; import { waitFor } from "./utils.js";
import { serverDefaults } from "../config.js";
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`); const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
@@ -30,9 +31,9 @@ async function setup<
| (AccountClass<Account> & CoValueFromRaw<Account>) | (AccountClass<Account> & CoValueFromRaw<Account>)
| AnyAccountSchema, | AnyAccountSchema,
>(AccountSchema?: S) { >(AccountSchema?: S) {
const { server, port } = await setupSyncServer(); const { server, port, host } = await setupSyncServer();
const syncServer = `ws://localhost:${port}`; const syncServer = `ws://${host}:${port}`;
const { worker, done, waitForConnection, subscribeToConnectionChange } = const { worker, done, waitForConnection, subscribeToConnectionChange } =
await setupWorker(syncServer, AccountSchema); await setupWorker(syncServer, AccountSchema);
@@ -43,13 +44,18 @@ async function setup<
syncServer, syncServer,
server, server,
port, port,
host,
waitForConnection, waitForConnection,
subscribeToConnectionChange, subscribeToConnectionChange,
}; };
} }
async function setupSyncServer(defaultPort = "0") { async function setupSyncServer(
defaultHost = serverDefaults.host,
defaultPort = "0",
) {
const server = await startSyncServer({ const server = await startSyncServer({
host: defaultHost,
port: defaultPort, port: defaultPort,
inMemory: false, inMemory: false,
db: dbPath, db: dbPath,
@@ -61,7 +67,7 @@ async function setupSyncServer(defaultPort = "0") {
server.close(); server.close();
}); });
return { server, port }; return { server, port, host: defaultHost };
} }
async function setupWorker< async function setupWorker<
@@ -258,6 +264,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port // Start a new sync server on the same port
const newServer = await startSyncServer({ const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port, port: worker1.port,
inMemory: true, inMemory: true,
db: "", db: "",
@@ -290,6 +297,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port // Start a new sync server on the same port
const newServer = await startSyncServer({ const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port, port: worker1.port,
inMemory: true, inMemory: true,
db: "", db: "",
@@ -326,6 +334,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port // Start a new sync server on the same port
const newServer = await startSyncServer({ const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port, port: worker1.port,
inMemory: true, inMemory: true,
db: "", db: "",

View File

@@ -0,0 +1,4 @@
import { type Server } from "node:http";
import { type LocalNode } from "cojson";
export type SyncServer = Server & { localNode: LocalNode };

View File

@@ -1,5 +1,34 @@
# jazz-tools # jazz-tools
## 0.17.9
### Patch Changes
- 52ea0c7: Explicit loadAs in upsertUnique to use it without loaded context
- Updated dependencies [7586c3b]
- cojson@0.17.9
- cojson-storage-indexeddb@0.17.9
- cojson-transport-ws@0.17.9
## 0.17.8
### Patch Changes
- ac3e694: Fixed an issue where CoValue schemas could be incorrectly passed to `co.map` and `co.profile` schema definers.
- 6dbb053: Prosemirror: fix RangeError triggered when creating invalid HTML
- 1a182f0: Add missing BaseProfileShape export
- cojson@0.17.8
- cojson-storage-indexeddb@0.17.8
- cojson-transport-ws@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
- cojson-storage-indexeddb@0.17.7
- cojson-transport-ws@0.17.7
## 0.17.6 ## 0.17.6
### Patch Changes ### Patch Changes

View File

@@ -140,7 +140,7 @@
}, },
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "0.17.6", "version": "0.17.9",
"dependencies": { "dependencies": {
"@manuscripts/prosemirror-recreate-steps": "^0.1.4", "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
"@scure/base": "1.2.1", "@scure/base": "1.2.1",

View File

@@ -1,6 +1,6 @@
import { recreateTransform } from "@manuscripts/prosemirror-recreate-steps"; import { recreateTransform } from "@manuscripts/prosemirror-recreate-steps";
import { CoRichText } from "jazz-tools"; import { CoRichText } from "jazz-tools";
import { Transaction } from "prosemirror-state"; import { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view"; import { EditorView } from "prosemirror-view";
import { htmlToProseMirror, proseMirrorToHtml } from "./converter.js"; import { htmlToProseMirror, proseMirrorToHtml } from "./converter.js";
@@ -34,6 +34,8 @@ export const META_KEY = "fromJazz";
export function createSyncHandlers(coRichText: CoRichText | undefined) { export function createSyncHandlers(coRichText: CoRichText | undefined) {
// Store the editor view in a closure // Store the editor view in a closure
let view: EditorView | undefined; let view: EditorView | undefined;
let localChange = false;
let remoteChange = false;
/** /**
* Handles changes from CoRichText by updating the ProseMirror editor. * Handles changes from CoRichText by updating the ProseMirror editor.
@@ -47,24 +49,47 @@ export function createSyncHandlers(coRichText: CoRichText | undefined) {
* @param newText - The updated CoRichText instance * @param newText - The updated CoRichText instance
*/ */
function handleCoRichTextChange(newText: CoRichText) { function handleCoRichTextChange(newText: CoRichText) {
if (!view || !newText) return; if (!view || !newText || localChange || remoteChange) return;
const pmDoc = htmlToProseMirror( const currentView = view;
newText.toString(), remoteChange = true;
view.state.doc.type.schema,
);
const transform = recreateTransform(view.state.doc, pmDoc);
// Create a new transaction // Changes on CoPlainText are emitted word by word, which means that it creates
const tr = view.state.tr; // invalid intermediate states when wrapping a document with HTML tags
// To fix the issue, we throttle the changes to the next microtask
queueMicrotask(() => {
const pmDoc = htmlToProseMirror(
newText.toString(),
currentView.state.doc.type.schema,
);
// Apply all steps from the transform to the transaction try {
transform.steps.forEach((step) => { const transform = recreateTransform(currentView.state.doc, pmDoc);
tr.step(step);
// Create a new transaction
const tr = currentView.state.tr;
// Apply all steps from the transform to the transaction
transform.steps.forEach((step) => {
tr.step(step);
});
tr.setMeta(META_KEY, true);
currentView.dispatch(tr);
} catch (err) {
// Sometimes recreateTransform fails, so we just rebuild the doc from scratch
const newState = EditorState.create({
schema: currentView.state.schema,
doc: pmDoc,
plugins: currentView.state.plugins,
selection: currentView.state.selection,
});
currentView.updateState(newState);
} finally {
remoteChange = false;
}
}); });
tr.setMeta(META_KEY, true);
view.dispatch(tr);
} }
/** /**
@@ -82,7 +107,12 @@ export function createSyncHandlers(coRichText: CoRichText | undefined) {
if (tr.docChanged) { if (tr.docChanged) {
const str = proseMirrorToHtml(tr.doc); const str = proseMirrorToHtml(tr.doc);
coRichText.applyDiff(str); localChange = true;
try {
coRichText.applyDiff(str);
} finally {
localChange = false;
}
} }
} }

View File

@@ -1,29 +1,33 @@
// @vitest-environment jsdom // @vitest-environment jsdom
import { Account, CoRichText } from "jazz-tools"; import { CoRichText } from "jazz-tools";
import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing"; import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
import { schema } from "prosemirror-schema-basic";
import { EditorState, TextSelection } from "prosemirror-state"; import { EditorState, TextSelection } from "prosemirror-state";
import { Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view"; import { EditorView } from "prosemirror-view";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import {
afterEach,
beforeEach,
describe,
expect,
it,
onTestFinished,
} from "vitest";
import { createJazzPlugin } from "../lib/plugin"; import { createJazzPlugin } from "../lib/plugin";
import { Schema } from "prosemirror-model";
import { schema as basicSchema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
let account: Account; const schema = new Schema({
let coRichText: CoRichText; nodes: addListNodes(basicSchema.spec.nodes, "paragraph block*", "block"),
let plugin: Plugin; marks: basicSchema.spec.marks,
let state: EditorState; });
let view: EditorView;
beforeEach(async () => {
await setupJazzTestSync();
account = await createJazzTestAccount({ isCurrentActiveAccount: true });
async function setupTest(initialContent = "<p>Hello</p>") {
// Create a real CoRichText with the test account as owner // Create a real CoRichText with the test account as owner
coRichText = CoRichText.create("<p>Hello</p>", account); const coRichText = CoRichText.create(initialContent);
plugin = createJazzPlugin(coRichText); const plugin = createJazzPlugin(coRichText);
state = EditorState.create({ const state = EditorState.create({
schema, schema,
plugins: [plugin], plugins: [plugin],
}); });
@@ -33,25 +37,32 @@ beforeEach(async () => {
document.body.appendChild(editorElement); document.body.appendChild(editorElement);
// Initialize the editor view // Initialize the editor view
view = new EditorView(editorElement, { const view = new EditorView(editorElement, {
state, state,
}); });
});
afterEach(() => { onTestFinished(() => {
// Clean up the editor view
if (view) {
view.destroy(); view.destroy();
view.dom.remove(); editorElement.remove();
} });
return { coRichText, plugin, state, view, editorElement };
}
beforeEach(async () => {
await setupJazzTestSync();
await createJazzTestAccount({ isCurrentActiveAccount: true });
}); });
describe("createJazzPlugin", () => { describe("createJazzPlugin", () => {
it("initializes editor with CoRichText content", () => { it("initializes editor with CoRichText content", async () => {
const { state } = await setupTest();
expect(state.doc.textContent).toContain("Hello"); expect(state.doc.textContent).toContain("Hello");
}); });
it("updates editor when CoRichText changes", async () => { it("updates editor when CoRichText changes", async () => {
const { coRichText, view } = await setupTest();
// Update CoRichText content // Update CoRichText content
coRichText.applyDiff("<p>Updated content</p>"); coRichText.applyDiff("<p>Updated content</p>");
@@ -61,7 +72,9 @@ describe("createJazzPlugin", () => {
expect(view.state.doc.textContent).toContain("Updated content"); expect(view.state.doc.textContent).toContain("Updated content");
}); });
it("updates CoRichText when editor content changes", () => { it("updates CoRichText when editor content changes", async () => {
const { coRichText, view } = await setupTest();
// Create a transaction to update the editor content // Create a transaction to update the editor content
const tr = view.state.tr.insertText(" World", 6); const tr = view.state.tr.insertText(" World", 6);
view.dispatch(tr); view.dispatch(tr);
@@ -70,8 +83,8 @@ describe("createJazzPlugin", () => {
expect(coRichText.toString()).toContain("Hello World"); expect(coRichText.toString()).toContain("Hello World");
}); });
it("handles empty CoRichText initialization", () => { it("handles empty CoRichText initialization", async () => {
const emptyCoRichText = CoRichText.create("", account); const emptyCoRichText = CoRichText.create("");
const emptyPlugin = createJazzPlugin(emptyCoRichText); const emptyPlugin = createJazzPlugin(emptyCoRichText);
const emptyState = EditorState.create({ const emptyState = EditorState.create({
schema, schema,
@@ -81,7 +94,7 @@ describe("createJazzPlugin", () => {
expect(emptyState.doc.textContent).toBe(""); expect(emptyState.doc.textContent).toBe("");
}); });
it("handles undefined CoRichText", () => { it("handles undefined CoRichText", async () => {
const undefinedPlugin = createJazzPlugin(undefined); const undefinedPlugin = createJazzPlugin(undefined);
const undefinedState = EditorState.create({ const undefinedState = EditorState.create({
schema, schema,
@@ -91,7 +104,9 @@ describe("createJazzPlugin", () => {
expect(undefinedState.doc.textContent).toBe(""); expect(undefinedState.doc.textContent).toBe("");
}); });
it("prevents infinite update loops", () => { it("prevents infinite update loops", async () => {
const { coRichText, view } = await setupTest();
// Create a transaction that would normally trigger a CoRichText update // Create a transaction that would normally trigger a CoRichText update
const tr = view.state.tr.insertText(" Loop", 6); const tr = view.state.tr.insertText(" Loop", 6);
@@ -106,7 +121,9 @@ describe("createJazzPlugin", () => {
expect(coRichText.toString()).not.toContain("Loop"); expect(coRichText.toString()).not.toContain("Loop");
}); });
it.skip("preserves selection when CoRichText changes", () => { it("preserves selection when CoRichText changes", async () => {
const { coRichText, view } = await setupTest();
// Set a selection in the editor // Set a selection in the editor
const tr = view.state.tr.setSelection( const tr = view.state.tr.setSelection(
TextSelection.create(view.state.doc, 2, 5), TextSelection.create(view.state.doc, 2, 5),
@@ -118,10 +135,49 @@ describe("createJazzPlugin", () => {
expect(view.state.selection.to).toBe(5); expect(view.state.selection.to).toBe(5);
// Update CoRichText content // Update CoRichText content
coRichText.applyDiff("<p>Updated content</p>"); coRichText.applyDiff("<p>Hello world</p>");
await new Promise((resolve) => setTimeout(resolve, 0));
// Verify selection is preserved after content update // Verify selection is preserved after content update
expect(view.state.selection.from).toBe(2); expect(view.state.selection.from).toBe(2);
expect(view.state.selection.to).toBe(5); expect(view.state.selection.to).toBe(5);
}); });
it("falls back to creating a new EditorState when the transform fails", async () => {
const { coRichText, editorElement } = await setupTest(
"<p>A <strong>hu<em>man</strong></em>.</p>",
);
// Wait for the next tick to allow the update to propagate
await new Promise((resolve) => setTimeout(resolve, 0));
// Update CoRichText content
coRichText.applyDiff(
"<ol><li><p>A <strong>hu</strong><em><strong>man</strong></em>.</p></li></ol>",
);
// Wait for the next tick to allow the update to propagate
await new Promise((resolve) => setTimeout(resolve, 0));
expect(editorElement.querySelector(".ProseMirror")?.innerHTML).toBe(
"<ol><li><p>A <strong>hu</strong><em><strong>man</strong></em>.</p></li></ol>",
);
});
it("handles updates with emojis", async () => {
const { coRichText, editorElement } = await setupTest(
"<p>A <strong>hu</strong><em><strong>man</strong></em>.</p>",
);
// Update CoRichText content
coRichText.applyDiff("<p>A human💪</p>");
// Wait for the next tick to allow the update to propagate
await new Promise((resolve) => setTimeout(resolve, 0));
expect(editorElement.querySelector(".ProseMirror")?.innerHTML).toBe(
"<p>A human💪</p>",
);
});
}); });

View File

@@ -9,7 +9,7 @@ import {
} from "jazz-tools"; } from "jazz-tools";
import { LocalStorageKVStore } from "jazz-tools/browser"; import { LocalStorageKVStore } from "jazz-tools/browser";
import { useAuthSecretStorage, useJazzContext } from "jazz-tools/react-core"; import { useAuthSecretStorage, useJazzContext } from "jazz-tools/react-core";
import { useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { JazzProviderProps, JazzReactProvider } from "../provider.js"; import { JazzProviderProps, JazzReactProvider } from "../provider.js";
function useJazzClerkAuth(clerk: MinimalClerkClient) { function useJazzClerkAuth(clerk: MinimalClerkClient) {
@@ -43,7 +43,9 @@ export const JazzReactProviderWithClerk = <
| (AccountClass<Account> & CoValueFromRaw<Account>) | (AccountClass<Account> & CoValueFromRaw<Account>)
| AnyAccountSchema, | AnyAccountSchema,
>( >(
props: { clerk: MinimalClerkClient } & JazzProviderProps<S>, props: {
clerk: MinimalClerkClient;
} & JazzProviderProps<S>,
) => { ) => {
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
@@ -61,7 +63,7 @@ export const JazzReactProviderWithClerk = <
}, []); }, []);
if (!isLoaded) { if (!isLoaded) {
return null; return props.fallback ?? null;
} }
return ( return (

View File

@@ -20,6 +20,7 @@ export type JazzProviderProps<
> = { > = {
children: React.ReactNode; children: React.ReactNode;
enableSSR?: boolean; enableSSR?: boolean;
fallback?: React.ReactNode | null;
} & JazzContextManagerProps<S>; } & JazzContextManagerProps<S>;
/** @category Context & Hooks */ /** @category Context & Hooks */
@@ -38,6 +39,7 @@ export function JazzReactProvider<
logOutReplacement, logOutReplacement,
onAnonymousAccountDiscarded, onAnonymousAccountDiscarded,
enableSSR, enableSSR,
fallback = null,
}: JazzProviderProps<S>) { }: JazzProviderProps<S>) {
const [contextManager] = React.useState( const [contextManager] = React.useState(
() => () =>
@@ -100,7 +102,7 @@ export function JazzReactProvider<
return ( return (
<JazzContext.Provider value={value}> <JazzContext.Provider value={value}>
<JazzContextManagerContext.Provider value={contextManager}> <JazzContextManagerContext.Provider value={contextManager}>
{value && children} {value ? children : fallback}
</JazzContextManagerContext.Provider> </JazzContextManagerContext.Provider>
</JazzContext.Provider> </JazzContext.Provider>
); );

View File

@@ -628,7 +628,11 @@ export class CoMap extends CoValueBase implements CoValue {
resolve?: RefsToResolveStrict<M, R>; resolve?: RefsToResolveStrict<M, R>;
}, },
): Promise<Resolved<M, R> | null> { ): Promise<Resolved<M, R> | null> {
let mapId = CoMap._findUnique(options.unique, options.owner.id); const mapId = CoMap._findUnique(
options.unique,
options.owner.id,
options.owner._loadedAs,
);
let map: Resolved<M, R> | null = await loadCoValueWithoutMe(this, mapId, { let map: Resolved<M, R> | null = await loadCoValueWithoutMe(this, mapId, {
...options, ...options,
loadAs: options.owner._loadedAs, loadAs: options.owner._loadedAs,

View File

@@ -35,6 +35,7 @@ export type {
TextPos, TextPos,
AccountClass, AccountClass,
AccountCreationProps, AccountCreationProps,
BaseProfileShape,
} from "./internal.js"; } from "./internal.js";
export { export {

View File

@@ -38,9 +38,14 @@ import {
// Note: if you're editing this function, edit the `isAnyCoValueSchema` // Note: if you're editing this function, edit the `isAnyCoValueSchema`
// function in `zodReExport.ts` as well // function in `zodReExport.ts` as well
export function isAnyCoValueSchema( export function isAnyCoValueSchema(
schema: AnyZodOrCoValueSchema | CoValueClass, schema: unknown,
): schema is AnyCoreCoValueSchema { ): schema is AnyCoreCoValueSchema {
return "collaborative" in schema && schema.collaborative === true; return (
typeof schema === "object" &&
schema !== null &&
"collaborative" in schema &&
schema.collaborative === true
);
} }
export function isCoValueSchema( export function isCoValueSchema(

View File

@@ -19,6 +19,7 @@ import {
createCoreCoPlainTextSchema, createCoreCoPlainTextSchema,
createCoreFileStreamSchema, createCoreFileStreamSchema,
hydrateCoreCoValueSchema, hydrateCoreCoValueSchema,
isAnyCoValueSchema,
} from "../../internal.js"; } from "../../internal.js";
import { import {
CoDiscriminatedUnionSchema, CoDiscriminatedUnionSchema,
@@ -36,6 +37,11 @@ import { z } from "./zodReExport.js";
export const coMapDefiner = <Shape extends z.core.$ZodLooseShape>( export const coMapDefiner = <Shape extends z.core.$ZodLooseShape>(
shape: Shape, shape: Shape,
): CoMapSchema<Shape> => { ): CoMapSchema<Shape> => {
if (isAnyCoValueSchema(shape as any)) {
throw new Error(
"co.map() expects an object as its argument, not a CoValue schema",
);
}
const coreSchema = createCoreCoMapSchema(shape); const coreSchema = createCoreCoMapSchema(shape);
return hydrateCoreCoValueSchema(coreSchema); return hydrateCoreCoValueSchema(coreSchema);
}; };
@@ -116,6 +122,11 @@ export const coProfileDefiner = <
>( >(
shape: Shape & Partial<DefaultProfileShape> = {} as any, shape: Shape & Partial<DefaultProfileShape> = {} as any,
): CoProfileSchema<Shape> => { ): CoProfileSchema<Shape> => {
if (isAnyCoValueSchema(shape as any)) {
throw new Error(
"co.profile() expects an object as its argument, not a CoValue schema",
);
}
const ehnancedShape = Object.assign(shape, { const ehnancedShape = Object.assign(shape, {
name: z.string(), name: z.string(),
inbox: z.optional(z.string()), inbox: z.optional(z.string()),

View File

@@ -88,6 +88,11 @@ function containsCoValueSchema(shape?: core.$ZodLooseShape): boolean {
// Note: if you're editing this function, edit the `isAnyCoValueSchema` // Note: if you're editing this function, edit the `isAnyCoValueSchema`
// function in `zodSchemaToCoSchema.ts` as well // function in `zodSchemaToCoSchema.ts` as well
function isAnyCoValueSchema(schema: any): boolean { function isAnyCoValueSchema(schema: unknown): boolean {
return "collaborative" in schema && schema.collaborative === true; return (
typeof schema === "object" &&
schema !== null &&
"collaborative" in schema &&
schema.collaborative === true
);
} }

View File

@@ -137,6 +137,12 @@ test("loading raw accounts should work", async () => {
expect(loadedAccount.profile!.name).toBe("test 1"); expect(loadedAccount.profile!.name).toBe("test 1");
}); });
test("co.profile() should throw an error if passed a CoValue schema", async () => {
expect(() => co.profile(co.map({}))).toThrow(
"co.profile() expects an object as its argument, not a CoValue schema",
);
});
test("should support recursive props on co.profile", async () => { test("should support recursive props on co.profile", async () => {
const User = co.profile({ const User = co.profile({
name: z.string(), name: z.string(),

View File

@@ -2325,6 +2325,12 @@ describe("co.map schema", () => {
expect(draftPerson.extraField).toEqual("extra"); expect(draftPerson.extraField).toEqual("extra");
}); });
}); });
test("co.map() should throw an error if passed a CoValue schema", () => {
expect(() => co.map(co.map({}))).toThrow(
"co.map() expects an object as its argument, not a CoValue schema",
);
});
}); });
describe("Updating a nested reference", () => { describe("Updating a nested reference", () => {

8
pnpm-lock.yaml generated
View File

@@ -2038,19 +2038,19 @@ importers:
specifier: ^0.25.5 specifier: ^0.25.5
version: 0.25.8(effect@3.11.9) version: 0.25.8(effect@3.11.9)
cojson: cojson:
specifier: workspace:0.17.6 specifier: workspace:0.17.9
version: link:../cojson version: link:../cojson
cojson-storage-sqlite: cojson-storage-sqlite:
specifier: workspace:0.17.6 specifier: workspace:0.17.9
version: link:../cojson-storage-sqlite version: link:../cojson-storage-sqlite
cojson-transport-ws: cojson-transport-ws:
specifier: workspace:0.17.6 specifier: workspace:0.17.9
version: link:../cojson-transport-ws version: link:../cojson-transport-ws
effect: effect:
specifier: ^3.6.5 specifier: ^3.6.5
version: 3.11.9 version: 3.11.9
jazz-tools: jazz-tools:
specifier: workspace:0.17.6 specifier: workspace:0.17.9
version: link:../jazz-tools version: link:../jazz-tools
ws: ws:
specifier: ^8.14.2 specifier: ^8.14.2

View File

@@ -1,5 +1,27 @@
# jazz-react-tailwind-starter # jazz-react-tailwind-starter
## 0.0.153
### Patch Changes
- Updated dependencies [52ea0c7]
- jazz-tools@0.17.9
## 0.0.152
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
## 0.0.151
### Patch Changes
- jazz-tools@0.17.7
## 0.0.150 ## 0.0.150
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "jazz-react-passkey-auth-starter", "name": "jazz-react-passkey-auth-starter",
"private": true, "private": true,
"version": "0.0.150", "version": "0.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,27 @@
# svelte-passkey-auth # svelte-passkey-auth
## 0.0.127
### Patch Changes
- Updated dependencies [52ea0c7]
- jazz-tools@0.17.9
## 0.0.126
### Patch Changes
- Updated dependencies [ac3e694]
- Updated dependencies [6dbb053]
- Updated dependencies [1a182f0]
- jazz-tools@0.17.8
## 0.0.125
### Patch Changes
- jazz-tools@0.17.7
## 0.0.124 ## 0.0.124
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "svelte-passkey-auth", "name": "svelte-passkey-auth",
"version": "0.0.124", "version": "0.0.127",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {