Compare commits

..

16 Commits

Author SHA1 Message Date
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
NicoR
ac3e694f4e fix: prevent passing CoValue schemas to co.map and co.profile 2025-08-19 11:05:54 -03: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
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
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
45 changed files with 508 additions and 96 deletions

View File

@@ -1,5 +1,20 @@
# passkey-svelte
## 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
### Patch Changes

View File

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

View File

@@ -46,6 +46,7 @@ In this case, provide the WebSocket endpoint your proxy exposes as the sync serv
### 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.
- `--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`.

View File

@@ -1,5 +1,17 @@
# cojson-storage-indexeddb
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# cojson-storage-sqlite
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# cojson-transport-nodejs-ws
## 0.17.8
### Patch Changes
- cojson@0.17.8
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes

View File

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

View File

@@ -1,5 +1,9 @@
# cojson
## 0.17.8
## 0.17.7
## 0.17.6
## 0.17.5

View File

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

View File

@@ -1,5 +1,22 @@
# jazz-react
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,24 @@
# jazz-auth-betterauth
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# jazz-betterauth-client-plugin
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,22 @@
# jazz-betterauth-server-plugin
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,26 @@
# jazz-react-auth-betterauth
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,27 @@
# jazz-run
## 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
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.17.6",
"version": "0.17.8",
"exports": {
"./startSyncServer": {
"types": "./dist/startSyncServer.d.ts",
@@ -28,11 +28,11 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.17.6",
"cojson-storage-sqlite": "workspace:0.17.6",
"cojson-transport-ws": "workspace:0.17.6",
"cojson": "workspace:0.17.8",
"cojson-storage-sqlite": "workspace:0.17.8",
"cojson-transport-ws": "workspace:0.17.8",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.17.6",
"jazz-tools": "workspace:0.17.8",
"ws": "^8.14.2"
},
"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 { createWorkerAccount } from "./createWorkerAccount.js";
import { startSyncServer } from "./startSyncServer.js";
import { serverDefaults } from "./config.js";
const jazzTools = Command.make("jazz-tools");
@@ -39,36 +40,63 @@ const accountCommand = Command.make("account").pipe(
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")
.pipe(Options.withAlias("p"))
.pipe(
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(
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")
.pipe(
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(
"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* () {
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(
`COJSON sync server listening on ws://127.0.0.1:${port}`,
`COJSON sync server listening on ws://${socketAddress}`,
);
// 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 { dirname } from "node:path";
import { LocalNode } from "cojson";
@@ -6,16 +6,19 @@ import { getBetterSqliteStorage } from "cojson-storage-sqlite";
import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { WebSocketServer } from "ws";
import { type SyncServer } from "./types.js";
export const startSyncServer = async ({
host,
port,
inMemory,
db,
}: {
host: string | undefined;
port: string | undefined;
inMemory: boolean;
db: string;
}) => {
}): Promise<SyncServer> => {
const crypto = await WasmCrypto.create();
const server = createServer((req, res) => {
@@ -94,8 +97,6 @@ export const startSyncServer = async ({
localNode.gracefulShutdown();
});
server.listen(port ? parseInt(port) : undefined);
const _close = server.close;
server.close = () => {
@@ -106,5 +107,11 @@ export const startSyncServer = async ({
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 { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
describe("createWorkerAccount - integration tests", () => {
it("should create a worker account using the local sync server", async () => {
// Pass port: undefined to let the server choose a random port
const server = await startSyncServer({
host: serverDefaults.host,
port: undefined,
inMemory: true,
db: "",

View File

@@ -1,24 +1,29 @@
import { randomUUID } from "crypto";
import { tmpdir } from "os";
import { join } from "path";
import { LocalNode } from "cojson";
import { co, z } from "jazz-tools";
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 { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
import { unlinkSync } from "node:fs";
const TestMap = co.map({
value: z.string(),
});
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
afterAll(() => {
unlinkSync(dbPath);
});
describe("startSyncServer", () => {
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
const server1 = await startSyncServer({
host: serverDefaults.host,
port: "0", // Random available port
inMemory: false,
db: dbPath,
@@ -48,6 +53,7 @@ describe("startSyncServer", () => {
// Start second server instance with same DB
const server2 = await startSyncServer({
host: serverDefaults.host,
port: "0",
inMemory: false,
db: dbPath,
@@ -74,4 +80,21 @@ describe("startSyncServer", () => {
await worker2.done();
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 { startSyncServer } from "../startSyncServer.js";
import { waitFor } from "./utils.js";
import { serverDefaults } from "../config.js";
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
@@ -30,9 +31,9 @@ async function setup<
| (AccountClass<Account> & CoValueFromRaw<Account>)
| AnyAccountSchema,
>(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 } =
await setupWorker(syncServer, AccountSchema);
@@ -43,13 +44,18 @@ async function setup<
syncServer,
server,
port,
host,
waitForConnection,
subscribeToConnectionChange,
};
}
async function setupSyncServer(defaultPort = "0") {
async function setupSyncServer(
defaultHost = serverDefaults.host,
defaultPort = "0",
) {
const server = await startSyncServer({
host: defaultHost,
port: defaultPort,
inMemory: false,
db: dbPath,
@@ -61,7 +67,7 @@ async function setupSyncServer(defaultPort = "0") {
server.close();
});
return { server, port };
return { server, port, host: defaultHost };
}
async function setupWorker<
@@ -258,6 +264,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
db: "",
@@ -290,6 +297,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
db: "",
@@ -326,6 +334,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
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,24 @@
# jazz-tools
## 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
### Patch Changes

View File

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

View File

@@ -1,6 +1,6 @@
import { recreateTransform } from "@manuscripts/prosemirror-recreate-steps";
import { CoRichText } from "jazz-tools";
import { Transaction } from "prosemirror-state";
import { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { htmlToProseMirror, proseMirrorToHtml } from "./converter.js";
@@ -34,6 +34,8 @@ export const META_KEY = "fromJazz";
export function createSyncHandlers(coRichText: CoRichText | undefined) {
// Store the editor view in a closure
let view: EditorView | undefined;
let localChange = false;
let remoteChange = false;
/**
* 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
*/
function handleCoRichTextChange(newText: CoRichText) {
if (!view || !newText) return;
if (!view || !newText || localChange || remoteChange) return;
const pmDoc = htmlToProseMirror(
newText.toString(),
view.state.doc.type.schema,
);
const transform = recreateTransform(view.state.doc, pmDoc);
const currentView = view;
remoteChange = true;
// Create a new transaction
const tr = view.state.tr;
// Changes on CoPlainText are emitted word by word, which means that it creates
// 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
transform.steps.forEach((step) => {
tr.step(step);
try {
const transform = recreateTransform(currentView.state.doc, pmDoc);
// 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) {
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
import { Account, CoRichText } from "jazz-tools";
import { CoRichText } from "jazz-tools";
import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
import { schema } from "prosemirror-schema-basic";
import { EditorState, TextSelection } from "prosemirror-state";
import { Plugin } from "prosemirror-state";
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 { Schema } from "prosemirror-model";
import { schema as basicSchema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
let account: Account;
let coRichText: CoRichText;
let plugin: Plugin;
let state: EditorState;
let view: EditorView;
beforeEach(async () => {
await setupJazzTestSync();
account = await createJazzTestAccount({ isCurrentActiveAccount: true });
const schema = new Schema({
nodes: addListNodes(basicSchema.spec.nodes, "paragraph block*", "block"),
marks: basicSchema.spec.marks,
});
async function setupTest(initialContent = "<p>Hello</p>") {
// 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);
state = EditorState.create({
const plugin = createJazzPlugin(coRichText);
const state = EditorState.create({
schema,
plugins: [plugin],
});
@@ -33,25 +37,32 @@ beforeEach(async () => {
document.body.appendChild(editorElement);
// Initialize the editor view
view = new EditorView(editorElement, {
const view = new EditorView(editorElement, {
state,
});
});
afterEach(() => {
// Clean up the editor view
if (view) {
onTestFinished(() => {
view.destroy();
view.dom.remove();
}
editorElement.remove();
});
return { coRichText, plugin, state, view, editorElement };
}
beforeEach(async () => {
await setupJazzTestSync();
await createJazzTestAccount({ isCurrentActiveAccount: true });
});
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");
});
it("updates editor when CoRichText changes", async () => {
const { coRichText, view } = await setupTest();
// Update CoRichText content
coRichText.applyDiff("<p>Updated content</p>");
@@ -61,7 +72,9 @@ describe("createJazzPlugin", () => {
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
const tr = view.state.tr.insertText(" World", 6);
view.dispatch(tr);
@@ -70,8 +83,8 @@ describe("createJazzPlugin", () => {
expect(coRichText.toString()).toContain("Hello World");
});
it("handles empty CoRichText initialization", () => {
const emptyCoRichText = CoRichText.create("", account);
it("handles empty CoRichText initialization", async () => {
const emptyCoRichText = CoRichText.create("");
const emptyPlugin = createJazzPlugin(emptyCoRichText);
const emptyState = EditorState.create({
schema,
@@ -81,7 +94,7 @@ describe("createJazzPlugin", () => {
expect(emptyState.doc.textContent).toBe("");
});
it("handles undefined CoRichText", () => {
it("handles undefined CoRichText", async () => {
const undefinedPlugin = createJazzPlugin(undefined);
const undefinedState = EditorState.create({
schema,
@@ -91,7 +104,9 @@ describe("createJazzPlugin", () => {
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
const tr = view.state.tr.insertText(" Loop", 6);
@@ -106,7 +121,9 @@ describe("createJazzPlugin", () => {
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
const tr = view.state.tr.setSelection(
TextSelection.create(view.state.doc, 2, 5),
@@ -118,10 +135,49 @@ describe("createJazzPlugin", () => {
expect(view.state.selection.to).toBe(5);
// 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
expect(view.state.selection.from).toBe(2);
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

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

View File

@@ -38,9 +38,14 @@ import {
// Note: if you're editing this function, edit the `isAnyCoValueSchema`
// function in `zodReExport.ts` as well
export function isAnyCoValueSchema(
schema: AnyZodOrCoValueSchema | CoValueClass,
schema: unknown,
): 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(

View File

@@ -19,6 +19,7 @@ import {
createCoreCoPlainTextSchema,
createCoreFileStreamSchema,
hydrateCoreCoValueSchema,
isAnyCoValueSchema,
} from "../../internal.js";
import {
CoDiscriminatedUnionSchema,
@@ -36,6 +37,11 @@ import { z } from "./zodReExport.js";
export const coMapDefiner = <Shape extends z.core.$ZodLooseShape>(
shape: 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);
return hydrateCoreCoValueSchema(coreSchema);
};
@@ -116,6 +122,11 @@ export const coProfileDefiner = <
>(
shape: Shape & Partial<DefaultProfileShape> = {} as any,
): 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, {
name: 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`
// function in `zodSchemaToCoSchema.ts` as well
function isAnyCoValueSchema(schema: any): boolean {
return "collaborative" in schema && schema.collaborative === true;
function isAnyCoValueSchema(schema: unknown): boolean {
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");
});
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 () => {
const User = co.profile({
name: z.string(),

View File

@@ -2325,6 +2325,12 @@ describe("co.map schema", () => {
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", () => {

8
pnpm-lock.yaml generated
View File

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

View File

@@ -1,5 +1,20 @@
# jazz-react-tailwind-starter
## 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
### Patch Changes

View File

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

View File

@@ -1,5 +1,20 @@
# svelte-passkey-auth
## 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
### Patch Changes

View File

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