Compare commits

...

20 Commits

Author SHA1 Message Date
Guido D'Orsi
937284f7e9 Merge pull request #2666 from garden-co/changeset-release/main
Version Packages
2025-07-24 17:16:01 +02:00
github-actions[bot]
e999727c70 Version Packages 2025-07-24 14:58:34 +00:00
Guido D'Orsi
2197766624 Merge pull request #2665 from garden-co/fix/optional-ref-assign
fix: property update when assigning an optional reference on CoMap
2025-07-24 16:56:05 +02:00
Guido D'Orsi
3fe53a3a4a fix: property update when assigning an optional reference on CoMap 2025-07-24 12:19:48 +02:00
Guido D'Orsi
867cb6b7a5 Merge pull request #2659 from garden-co/changeset-release/main
Version Packages
2025-07-23 18:30:51 +02:00
github-actions[bot]
0401fcf2a8 Version Packages 2025-07-23 12:43:56 +00:00
Nico Rainhart
139a649279 Merge pull request #2663 from garden-co/fix/export-WithHelpers-type
fix: Export `WithHelpers` type
2025-07-23 09:41:20 -03:00
NicoR
9acccb5df2 Add changeset 2025-07-23 09:23:12 -03:00
NicoR
fd90cdb49a fix: Export WithHelpers type 2025-07-23 09:15:09 -03:00
Giordano Ricci
df487d5335 Merge pull request #2658 from garden-co/gio/quint 2025-07-23 12:35:06 +01:00
Giordano Ricci
1efe84c691 Merge branch 'main' into gio/quint 2025-07-23 12:17:02 +01:00
Guido D'Orsi
063553090e docs: fix type errors on co.profile 2025-07-22 18:29:18 +02:00
Guido D'Orsi
6dffe73bd2 chore: rename jazz-paper-scissors in server-worker-inbox 2025-07-22 16:32:08 +02:00
Guido D'Orsi
68cb357a94 Merge pull request #2660 from garden-co/feat/persistent-peers
feat: introduce persistent peers
2025-07-22 16:18:56 +02:00
Guido D'Orsi
4f7bc91502 chore: apply suggestions from code review
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-22 14:40:45 +02:00
Guido D'Orsi
1e58ecb3ac test: integration tests for browser and workers on offline loading & sync 2025-07-22 12:42:49 +02:00
Guido D'Orsi
70ce7c5736 feat: introduce persistent peers 2025-07-22 11:12:48 +02:00
Nico Rainhart
6afdb16739 Merge pull request #2656 from garden-co/feat/prevent-resolving-discriminated-union-fields
feat: Prevent resolving discriminated union fields
2025-07-21 14:45:51 -03:00
NicoR
a584590ed8 Add changeset 2025-07-18 18:00:50 -03:00
NicoR
0a830e29a9 Prevent resolving discriminated union fields 2025-07-18 17:57:07 -03:00
91 changed files with 909 additions and 252 deletions

View File

@@ -1,5 +0,0 @@
---
"quint-ui": patch
---
Initial Quint-UI release

View File

@@ -14,7 +14,7 @@
"tests/jazz-svelte/src/**",
"examples/*svelte*/**",
"starters/*svelte*/**",
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
"examples/server-worker-inbox/src/routeTree.gen.ts",
"homepage/homepage/**",
"**/package.json"
]

View File

@@ -1,5 +1,20 @@
# passkey-svelte
## 0.0.104
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
## 0.0.103
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.102
### Patch Changes

View File

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

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -94,7 +94,7 @@ const MusicaAccountRoot = co.map({
const MusicaAccount = co.account({
root: MusicaAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
type MusicaAccount = co.loaded<typeof MusicaAccount>

View File

@@ -67,7 +67,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -130,7 +130,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---

View File

@@ -11,7 +11,7 @@ The main detail to understand when using Jazz server-side is that Server Workers
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/server-worker-inbox)
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>

View File

@@ -1,5 +1,18 @@
# cojson-storage-indexeddb
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,18 @@
# cojson-storage-sqlite
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,19 @@
# cojson-transport-nodejs-ws
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -165,6 +165,6 @@ export function createWebSocketPeer({
incoming,
outgoing,
role,
deletePeerStateOnClose,
persistent: !deletePeerStateOnClose,
};
}

View File

@@ -1,5 +1,13 @@
# cojson
## 0.15.15
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
## 0.15.13
## 0.15.12

View File

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

View File

@@ -97,6 +97,10 @@ export class PeerState {
return this.peer.incoming;
}
get persistent() {
return this.peer.persistent;
}
pushOutgoingMessage(msg: SyncMessage) {
this.peer.outgoing.push(msg);
}

View File

@@ -1056,37 +1056,33 @@ export class CoValueCore {
return;
}
const peersToActuallyLoadFrom = [] as PeerState[];
for (const peer of peers) {
const currentState = this.peers.get(peer.id)?.type;
const currentState = this.peers.get(peer.id)?.type ?? "unknown";
if (
!currentState ||
currentState === "unknown" ||
currentState === "unavailable"
) {
peersToActuallyLoadFrom.push(peer);
if (currentState === "unknown" || currentState === "unavailable") {
this.markPending(peer.id);
this.internalLoadFromPeer(peer);
}
}
for (const peer of peersToActuallyLoadFrom) {
this.internalLoadFromPeer(peer);
}
}
internalLoadFromPeer(peer: PeerState) {
if (peer.closed) {
if (peer.closed && !peer.persistent) {
this.markNotFoundInPeer(peer.id);
return;
}
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
/**
* On reconnection persistent peers will automatically fire the load request
* as part of the reconnection process.
*/
if (!peer.closed) {
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
}
return new Promise<void>((resolve) => {
const markNotFound = () => {
@@ -1100,7 +1096,9 @@ export class CoValueCore {
};
const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
const removeCloseListener = peer.addCloseListener(markNotFound);
const removeCloseListener = peer.persistent
? undefined
: peer.addCloseListener(markNotFound);
const listener = (state: CoValueCore) => {
const peerState = state.peers.get(peer.id);
@@ -1111,7 +1109,7 @@ export class CoValueCore {
peerState?.type === "unavailable"
) {
this.listeners.delete(listener);
removeCloseListener();
removeCloseListener?.();
clearTimeout(timeout);
resolve();
}

View File

@@ -13,6 +13,14 @@ export const CO_VALUE_LOADING_CONFIG = {
RETRY_DELAY: 3000,
};
export function setCoValueLoadingMaxRetries(maxRetries: number) {
CO_VALUE_LOADING_CONFIG.MAX_RETRIES = maxRetries;
}
export function setCoValueLoadingTimeout(timeout: number) {
CO_VALUE_LOADING_CONFIG.TIMEOUT = timeout;
}
export function setCoValueLoadingRetryDelay(delay: number) {
CO_VALUE_LOADING_CONFIG.RETRY_DELAY = delay;
}

View File

@@ -13,9 +13,11 @@ export function connectedPeers(
{
peer1role = "client",
peer2role = "client",
persistent = false,
}: {
peer1role?: Peer["role"];
peer2role?: Peer["role"];
persistent?: boolean;
} = {},
): [Peer, Peer] {
const from1to2 = new ConnectedPeerChannel();
@@ -26,6 +28,7 @@ export function connectedPeers(
incoming: from2to1,
outgoing: from1to2,
role: peer2role,
persistent,
};
const peer1AsPeer: Peer = {
@@ -33,6 +36,7 @@ export function connectedPeers(
incoming: from1to2,
outgoing: from2to1,
role: peer1role,
persistent,
};
return [peer1AsPeer, peer2AsPeer];

View File

@@ -88,7 +88,7 @@ export interface Peer {
outgoing: OutgoingPeerChannel;
role: "server" | "client";
priority?: number;
deletePeerStateOnClose?: boolean;
persistent?: boolean;
}
export function combinedKnownStates(
@@ -157,8 +157,7 @@ export class SyncManager {
getServerPeers(excludePeerId?: PeerID): PeerState[] {
return this.getPeers().filter(
(peer) =>
peer.role === "server" && peer.id !== excludePeerId && !peer.closed,
(peer) => peer.role === "server" && peer.id !== excludePeerId,
);
}
@@ -353,7 +352,7 @@ export class SyncManager {
unsubscribeFromKnownStatesUpdates();
this.peersCounter.add(-1, { role: peer.role });
if (peer.deletePeerStateOnClose && this.peers[peer.id] === peerState) {
if (!peer.persistent && this.peers[peer.id] === peerState) {
delete this.peers[peer.id];
}
});
@@ -794,8 +793,8 @@ export class SyncManager {
const peerState = this.peers[peerId];
// The peer has been closed, so it isn't possible to sync
if (!peerState || peerState.closed) {
// The peer has been closed and is not persistent, so it isn't possible to sync
if (!peerState) {
return;
}
@@ -830,7 +829,7 @@ export class SyncManager {
return this.local.storage?.waitForSync(id, this.local.getCoValue(id));
}
waitForSync(id: RawCoID, timeout = 30_000) {
waitForSync(id: RawCoID, timeout = 60_000) {
const peers = this.getPeers();
return Promise.all(

View File

@@ -251,9 +251,11 @@ describe("SyncStateManager", () => {
).toEqual({ uploaded: true });
});
test("should skip closed peers", async () => {
test("should skip non-persistent closed peers", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const { peerState } = client.connectToSyncServer({
persistent: false,
});
peerState.gracefulShutdown();
@@ -263,6 +265,42 @@ describe("SyncStateManager", () => {
await map.core.waitForSync();
});
test("should wait for persistent closed peers to reconnect", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer({
persistent: true,
});
peerState.gracefulShutdown();
const group = client.node.createGroup();
const map = group.createMap();
const promise = map.core.waitForSync().then(() => "waitForSync");
const result = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result).toBe("timeout");
client.connectToSyncServer({
persistent: true,
});
const result2 = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result2).toBe("waitForSync");
});
test("should skip client peers that are not subscribed to the coValue", async () => {
const server = setupTestNode({ isSyncServer: true });
const client = setupTestNode();

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { expectMap } from "../coValue";
import { setCoValueLoadingRetryDelay } from "../config";
import {
CO_VALUE_LOADING_CONFIG,
setCoValueLoadingRetryDelay,
} from "../config";
import { RawCoMap } from "../exports";
import {
SyncMessagesLog,
@@ -507,9 +510,12 @@ describe("loading coValues from server", () => {
`);
});
test("should mark the coValue as unavailable if the peer is closed", async () => {
test("should wait for a persistent peer to reconnect before marking the coValue as unavailable", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const connection1 = client.connectToSyncServer({
persistent: true,
});
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
@@ -517,13 +523,15 @@ describe("loading coValues from server", () => {
const map = group.createMap({
test: "value",
});
const promise = client.node.load(map.id);
// Close the peer connection
peerState.gracefulShutdown();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(await promise).toEqual("unavailable");
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
@@ -533,6 +541,60 @@ describe("loading coValues from server", () => {
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
test("should handle reconnections in the middle of a load with a persistent peer", async () => {
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
const map = group.createMap({
test: "value",
});
blockMessageTypeOnOutgoingPeer(connection1.peerOnServer, "content", {
id: map.id,
once: true,
});
const promise = client.node.load(map.id);
await new Promise((resolve) => setTimeout(resolve, 10));
// Close the peer connection
connection1.peerState.gracefulShutdown();
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | LOAD Map sessions: empty",
"client -> server | LOAD Group sessions: header/5",
"server -> client | KNOWN Group sessions: header/5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
@@ -909,4 +971,62 @@ describe("loading coValues from server", () => {
]
`);
});
test("should retry loading from a closed persistent peer after a timeout", async () => {
vi.useFakeTimers();
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
// Close the peer connection
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "reader");
const map = group.createMap();
map.set("hello", "world");
const promise = loadCoValueOrFail(client.node, map.id);
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
client.connectToSyncServer({
persistent: true,
});
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
vi.useRealTimers();
});
});

View File

@@ -26,6 +26,7 @@ function setupMesh() {
ourName: "edge-italy",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeItaly.addStorage({
ourName: "edge-italy",
@@ -36,6 +37,7 @@ function setupMesh() {
ourName: "edge-france",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeFrance.addStorage({
ourName: "edge-france",

View File

@@ -111,10 +111,12 @@ test("Can sync a coValue with private transactions through a server to another c
expect(mapOnClient2.get("hello")).toEqual("world");
});
test("should keep the peer state when the peer closes", async () => {
test("should keep the peer state when the peer closes if persistent is true", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();
@@ -133,12 +135,12 @@ test("should keep the peer state when the peer closes", async () => {
expect(syncManager.peers[peer.id]).not.toBeUndefined();
});
test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
test("should delete the peer state when the peer closes if persistent is false", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
peer.deletePeerStateOnClose = true;
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: false,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();

View File

@@ -429,6 +429,7 @@ export function getSyncServerConnectedPeer(opts: {
ourName?: string;
syncServer?: LocalNode;
peerId: string;
persistent?: boolean;
}) {
const currentSyncServer = opts?.syncServer ?? syncServer.current;
@@ -451,6 +452,7 @@ export function getSyncServerConnectedPeer(opts: {
role: "client",
name: opts.ourName,
},
persistent: opts?.persistent,
});
currentSyncServer.syncManager.addPeer(peer2);
@@ -483,6 +485,7 @@ export function setupTestNode(
syncServerName?: string;
ourName?: string;
syncServer?: LocalNode;
persistent?: boolean;
}) {
const { peer, peerStateOnServer, peerOnServer } =
getSyncServerConnectedPeer({
@@ -490,6 +493,7 @@ export function setupTestNode(
syncServerName: opts?.syncServerName,
ourName: opts?.ourName,
syncServer: opts?.syncServer,
persistent: opts?.persistent,
});
node.syncManager.addPeer(peer);
@@ -646,12 +650,22 @@ export type SyncTestMessage = {
export function connectedPeersWithMessagesTracking(opts: {
peer1: { id: string; role: Peer["role"]; name?: string };
peer2: { id: string; role: Peer["role"]; name?: string };
persistent?: boolean;
}) {
const [peer1, peer2] = connectedPeers(opts.peer1.id, opts.peer2.id, {
peer1role: opts.peer1.role,
peer2role: opts.peer2.role,
persistent: opts.persistent,
});
// If the persistent option is not provided, we default to true for the server and false for the client
// Trying to mimic the real world behavior of the sync server
if (opts.persistent === undefined) {
peer1.persistent = opts.peer1.role === "server";
peer2.persistent = opts.peer2.role === "server";
}
const peer1Push = peer1.outgoing.push;
peer1.outgoing.push = (msg) => {
if (typeof msg !== "string") {

View File

@@ -7890,7 +7890,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -7950,7 +7950,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---

View File

@@ -1,5 +1,25 @@
# jazz-auth-betterauth
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- jazz-betterauth-client-plugin@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# jazz-betterauth-client-plugin
## 0.15.15
### Patch Changes
- jazz-betterauth-server-plugin@0.15.15
## 0.15.14
### Patch Changes
- jazz-betterauth-server-plugin@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,23 @@
# jazz-betterauth-server-plugin
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,27 @@
# jazz-react-auth-betterauth
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- jazz-auth-betterauth@0.15.15
- jazz-betterauth-client-plugin@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-auth-betterauth@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -1,5 +1,27 @@
# jazz-run
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- cojson@0.15.15
- cojson-storage-sqlite@0.15.15
- cojson-transport-ws@0.15.15
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- jazz-tools@0.15.14
- cojson-storage-sqlite@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -230,8 +230,7 @@ describe("startWorker integration", () => {
await worker2.done();
});
// Flaky test, fails randomly on CI
test.skip("worker reconnects when sync server is closed and reopened", async () => {
test("worker reconnects when sync server is closed and reopened", async () => {
const worker1 = await setup();
const worker2 = await setupWorker(worker1.syncServer);
@@ -267,10 +266,6 @@ describe("startWorker integration", () => {
db: "",
});
// Wait for reconnection
await worker1.waitForConnection();
await worker2.waitForConnection();
await worker1.worker.waitForAllCoValuesSync();
// Verify both old and new values are synced

View File

@@ -1,5 +1,25 @@
# jazz-tools
## 0.15.15
### Patch Changes
- 3fe53a3: Fix property update when assigning an optional reference on CoMap
- cojson@0.15.15
- cojson-storage-indexeddb@0.15.15
- cojson-transport-ws@0.15.15
## 0.15.14
### Patch Changes
- a584590: Prevent resolving discriminated union fields
- 9acccb5: Export `WithHelpers` type used in CoValue schemas
- Updated dependencies [70ce7c5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- cojson-storage-indexeddb@0.15.14
## 0.15.13
### Patch Changes

View File

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

View File

@@ -2,11 +2,23 @@ import { SessionID } from "cojson";
import { ItemsSym } from "../internal.js";
import { type Account } from "./account.js";
import { CoFeedEntry } from "./coFeed.js";
import { type CoKeys, type CoMap } from "./coMap.js";
import { type CoKeys } from "./coMap.js";
import { type CoValue, type ID } from "./interfaces.js";
type NotNull<T> = Exclude<T, null>;
/**
* Used to check if T is a union type.
*
* If T is a union type, the left hand side of the extends becomes a union of function types.
* The right hand side is always a single function type.
*/
type IsUnion<T, U = T> = (T extends any ? (x: T) => void : never) extends (
x: U,
) => void
? false
: true;
export type RefsToResolve<
V,
DepthLimit extends number = 10,
@@ -16,56 +28,58 @@ export type RefsToResolve<
| (DepthLimit extends CurrentDepth["length"]
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
any
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
: IsUnion<NonNullable<V>> extends true
? true
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: boolean);
| boolean
: boolean);
export type RefsToResolveStrict<T, V> = V extends RefsToResolve<T>
? RefsToResolve<T>

View File

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

View File

@@ -341,17 +341,15 @@ export class SubscriptionScope<D extends CoValue> {
this.resolve = {};
}
if (this.resolve.$each || key in this.resolve) {
return;
if (!this.resolve.$each && !(key in this.resolve)) {
const resolve = this.resolve as Record<string, any>;
// Adding the key to the resolve object to resolve the key when calling loadChildren
resolve[key] = true;
// Track the keys that are autoloaded to flag any id on that key as autoloaded
this.autoloadedKeys.add(key);
}
const resolve = this.resolve as Record<string, any>;
// Adding the key to the resolve object to resolve the key when calling loadChildren
resolve[key] = true;
// Track the keys that are autoloaded to flag any id on that key as autoloaded
this.autoloadedKeys.add(key);
if (this.value.type !== "loaded") {
return;
}

View File

@@ -2103,3 +2103,144 @@ describe("CoMap migration", () => {
});
});
});
describe("Updating a nested reference", () => {
test("should assign a resolved optional reference and expect value is not null", async () => {
// Define the schema similar to the server-worker-http example
const PlaySelection = co.map({
value: z.literal(["rock", "paper", "scissors"]),
group: Group,
});
const Player = co.map({
account: co.account(),
playSelection: PlaySelection.optional(),
});
const Game = co.map({
player1: Player,
player2: Player,
outcome: z.literal(["player1", "player2", "draw"]).optional(),
player1Score: z.number(),
player2Score: z.number(),
});
// Create accounts for the players
const player1Account = await createJazzTestAccount({
creationProps: { name: "Player 1" },
});
const player2Account = await createJazzTestAccount({
creationProps: { name: "Player 2" },
});
// Create a game
const game = Game.create({
player1: Player.create({
account: player1Account,
}),
player2: Player.create({
account: player2Account,
}),
player1Score: 0,
player2Score: 0,
});
// Create a group for the play selection (similar to the route logic)
const group = Group.create({ owner: Account.getMe() });
group.addMember(player1Account, "reader");
// Load the game to verify the assignment worked
const loadedGame = await Game.load(game.id, {
resolve: {
player1: {
account: true,
playSelection: true,
},
player2: {
account: true,
playSelection: true,
},
},
});
assert(loadedGame);
// Create a play selection
const playSelection = PlaySelection.create({ value: "rock", group }, group);
// Assign the play selection to player1 (similar to the route logic)
loadedGame.player1.playSelection = playSelection;
// Verify that the playSelection is not null and has the expected value
expect(loadedGame.player1.playSelection).not.toBeNull();
expect(loadedGame.player1.playSelection).toBeDefined();
});
test("should assign a resolved reference and expect value to update", async () => {
// Define the schema similar to the server-worker-http example
const PlaySelection = co.map({
value: z.literal(["rock", "paper", "scissors"]),
});
const Player = co.map({
account: co.account(),
playSelection: PlaySelection,
});
const Game = co.map({
player1: Player,
player2: Player,
outcome: z.literal(["player1", "player2", "draw"]).optional(),
player1Score: z.number(),
player2Score: z.number(),
});
// Create accounts for the players
const player1Account = await createJazzTestAccount({
creationProps: { name: "Player 1" },
});
const player2Account = await createJazzTestAccount({
creationProps: { name: "Player 2" },
});
// Create a game
const game = Game.create({
player1: Player.create({
account: player1Account,
playSelection: PlaySelection.create({ value: "rock" }),
}),
player2: Player.create({
account: player2Account,
playSelection: PlaySelection.create({ value: "paper" }),
}),
player1Score: 0,
player2Score: 0,
});
// Load the game to verify the assignment worked
const loadedGame = await Game.load(game.id, {
resolve: {
player1: {
account: true,
playSelection: true,
},
player2: {
account: true,
playSelection: true,
},
},
});
assert(loadedGame);
// Create a play selection
const playSelection = PlaySelection.create({ value: "scissors" });
// Assign the play selection to player1 (similar to the route logic)
loadedGame.player1.playSelection = playSelection;
// Verify that the playSelection is not null and has the expected value
expect(loadedGame.player1.playSelection.id).toBe(playSelection.id);
expect(loadedGame.player1.playSelection.value).toEqual("scissors");
});
});

View File

@@ -381,6 +381,64 @@ test("The resolve type doesn't accept extra keys", async () => {
}
});
test("The resolve type accepts keys from optional fields", async () => {
const Person = co.map({
name: z.string(),
});
const Dog = co.map({
type: z.literal("dog"),
owner: Person.optional(),
});
const Pets = co.list(Dog);
const pets = await Pets.create([
Dog.create({ type: "dog", owner: Person.create({ name: "Rex" }) }),
]);
await pets.ensureLoaded({
resolve: {
$each: { owner: true },
},
});
expect(pets[0]?.owner?.name).toEqual("Rex");
});
test("The resolve type doesn't accept keys from discriminated unions", async () => {
const Person = co.map({
name: z.string(),
});
const Dog = co.map({
type: z.literal("dog"),
owner: Person,
});
const Cat = co.map({
type: z.literal("cat"),
});
const Pet = co.discriminatedUnion("type", [Dog, Cat]);
const Pets = co.list(Pet);
const pets = await Pets.create([
Dog.create({ type: "dog", owner: Person.create({ name: "Rex" }) }),
]);
await pets.ensureLoaded({
resolve: {
$each: true,
},
});
await pets.ensureLoaded({
// @ts-expect-error cannot resolve owner
resolve: { $each: { owner: true } },
});
expect(pets).toBeTruthy();
if (pets?.[0]?.type === "dog") {
expect(pets[0].owner?.name).toEqual("Rex");
}
});
describe("Deep loading with unauthorized account", async () => {
const bob = await createJazzTestAccount({
creationProps: { name: "Bob" },

View File

@@ -0,0 +1,7 @@
# quint-ui
## 0.0.1
### Patch Changes
- f61a120: Initial Quint-UI release

View File

@@ -1,9 +1,9 @@
{
"name": "quint-ui",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": {
".":{
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},

180
pnpm-lock.yaml generated
View File

@@ -852,82 +852,6 @@ importers:
specifier: ^5
version: 5.8.3
examples/jazz-paper-scissors:
dependencies:
'@radix-ui/react-label':
specifier: ^2.1.2
version: 2.1.7(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-separator':
specifier: ^1.1.2
version: 1.1.7(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.1.2
version: 1.2.3(@types/react@19.1.0)(react@19.1.0)
'@tailwindcss/vite':
specifier: ^4.0.17
version: 4.1.10(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
'@tanstack/react-router':
specifier: ^1.115.0
version: 1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-router-devtools':
specifier: ^1.114.29
version: 1.116.0(@tanstack/react-router@1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.115.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)
'@tanstack/router-plugin':
specifier: ^1.114.30
version: 1.116.1(@tanstack/react-router@1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
jazz-tools:
specifier: workspace:*
version: link:../../packages/jazz-tools
lucide-react:
specifier: ^0.485.0
version: 0.485.0(react@19.1.0)
react:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
tailwind-merge:
specifier: ^3.0.2
version: 3.3.0
tailwindcss:
specifier: ^4.0.17
version: 4.1.10
tw-animate-css:
specifier: ^1.2.5
version: 1.2.8
devDependencies:
'@types/react':
specifier: 19.1.0
version: 19.1.0
'@types/react-dom':
specifier: 19.1.0
version: 19.1.0(@types/react@19.1.0)
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.5.1(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
jazz-run:
specifier: workspace:*
version: link:../../packages/jazz-run
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
tsx:
specifier: ^4.19.3
version: 4.19.3
typescript:
specifier: ~5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
examples/multi-cursors:
dependencies:
'@react-spring/web':
@@ -1423,6 +1347,82 @@ importers:
specifier: 6.3.5
version: 6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
examples/server-worker-inbox:
dependencies:
'@radix-ui/react-label':
specifier: ^2.1.2
version: 2.1.7(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-separator':
specifier: ^1.1.2
version: 1.1.7(@types/react-dom@19.1.0(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.1.2
version: 1.2.3(@types/react@19.1.0)(react@19.1.0)
'@tailwindcss/vite':
specifier: ^4.0.17
version: 4.1.10(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
'@tanstack/react-router':
specifier: ^1.115.0
version: 1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-router-devtools':
specifier: ^1.114.29
version: 1.116.0(@tanstack/react-router@1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.115.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)
'@tanstack/router-plugin':
specifier: ^1.114.30
version: 1.116.1(@tanstack/react-router@1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
jazz-tools:
specifier: workspace:*
version: link:../../packages/jazz-tools
lucide-react:
specifier: ^0.485.0
version: 0.485.0(react@19.1.0)
react:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
tailwind-merge:
specifier: ^3.0.2
version: 3.3.0
tailwindcss:
specifier: ^4.0.17
version: 4.1.11
tw-animate-css:
specifier: ^1.2.5
version: 1.2.8
devDependencies:
'@types/react':
specifier: 19.1.0
version: 19.1.0
'@types/react-dom':
specifier: 19.1.0
version: 19.1.0(@types/react@19.1.0)
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.5.1(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1))
jazz-run:
specifier: workspace:*
version: link:../../packages/jazz-run
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
tsx:
specifier: ^4.19.3
version: 4.19.3
typescript:
specifier: ~5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
examples/todo:
dependencies:
'@radix-ui/react-checkbox':
@@ -1772,19 +1772,19 @@ importers:
specifier: ^0.25.5
version: 0.25.8(effect@3.11.9)
cojson:
specifier: workspace:0.15.13
specifier: workspace:0.15.15
version: link:../cojson
cojson-storage-sqlite:
specifier: workspace:0.15.13
specifier: workspace:0.15.15
version: link:../cojson-storage-sqlite
cojson-transport-ws:
specifier: workspace:0.15.13
specifier: workspace:0.15.15
version: link:../cojson-transport-ws
effect:
specifier: ^3.6.5
version: 3.11.9
jazz-tools:
specifier: workspace:0.15.13
specifier: workspace:0.15.15
version: link:../jazz-tools
ws:
specifier: ^8.14.2
@@ -13867,7 +13867,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.25.9':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.28.0
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
@@ -13942,7 +13942,7 @@ snapshots:
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.28.0
transitivePeerDependencies:
- supports-color
@@ -13995,7 +13995,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.25.9':
dependencies:
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.28.0
transitivePeerDependencies:
- supports-color
@@ -18757,7 +18757,7 @@ snapshots:
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.1)
'@babel/template': 7.27.2
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.28.0
'@tanstack/router-core': 1.115.3
'@tanstack/router-generator': 1.116.0(@tanstack/react-router@1.116.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
'@tanstack/router-utils': 1.115.0
@@ -19017,16 +19017,16 @@ snapshots:
'@types/babel__generator@7.6.8':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.28.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/types': 7.28.0
'@types/babel__traverse@7.20.6':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.28.0
'@types/better-sqlite3@7.6.12':
dependencies:
@@ -20125,7 +20125,7 @@ snapshots:
'@babel/core': 7.27.1
'@babel/parser': 7.27.2
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.28.0
transitivePeerDependencies:
- supports-color
@@ -23795,7 +23795,7 @@ snapshots:
'@babel/parser': 7.27.2
'@babel/template': 7.27.2
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
'@babel/types': 7.28.0
accepts: 1.3.8
chalk: 4.1.2
ci-info: 2.0.0
@@ -26990,7 +26990,7 @@ snapshots:
unplugin@2.3.2:
dependencies:
acorn: 8.14.1
acorn: 8.15.0
picomatch: 4.0.2
webpack-virtual-modules: 0.6.2

View File

@@ -1,5 +1,20 @@
# jazz-react-tailwind-starter
## 0.0.135
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
## 0.0.134
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.133
### Patch Changes

View File

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

View File

@@ -1,5 +1,20 @@
# svelte-passkey-auth
## 0.0.109
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
## 0.0.108
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.107
### Patch Changes

View File

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

View File

@@ -201,10 +201,10 @@ describe("Browser sync", () => {
await map.waitForSync();
await syncServer.setOffline(true);
await syncServer.setOnline(true);
onTestFinished(async () => {
await syncServer.setOffline(false);
await syncServer.setOnline(false);
});
// Clearing the credentials storage so the next auth will be a new account

View File

@@ -36,9 +36,11 @@ const disconnectAllClientsCommand: BrowserCommand<[url: string]> = async (
syncServer.disconnectAllClients();
};
const setOfflineCommand: BrowserCommand<
[url: string, active: boolean]
> = async (ctx, url, active) => {
const setOnlineCommand: BrowserCommand<[url: string, active: boolean]> = async (
ctx,
url,
active,
) => {
const syncServer = syncServers.get(url);
if (!syncServer) {
@@ -77,7 +79,7 @@ declare module "@vitest/browser/context" {
port: number;
}>;
disconnectAllClients: (url: string) => Promise<void>;
setOffline: (url: string, active: boolean) => Promise<void>;
setOnline: (url: string, active: boolean) => Promise<void>;
closeSyncServer: (url: string) => Promise<void>;
cleanup: () => Promise<void>;
}
@@ -86,7 +88,7 @@ declare module "@vitest/browser/context" {
export const customCommands = {
startSyncServer: startSyncServerCommand,
disconnectAllClients: disconnectAllClientsCommand,
setOffline: setOfflineCommand,
setOnline: setOnlineCommand,
closeSyncServer: closeSyncServerCommand,
cleanup: cleanupCommand,
};

View File

@@ -88,7 +88,7 @@ export async function startSyncServer(port?: number, dbName?: string) {
url,
port: syncServerPort,
disconnectAllClients: () => commands.disconnectAllClients(url),
setOffline: (active: boolean) => commands.setOffline(url, active),
setOnline: (active: boolean) => commands.setOnline(url, active),
close,
};
}

View File

@@ -1,28 +1,22 @@
import { commands } from "@vitest/browser/context";
import {
Account,
AuthSecretStorage,
CoMap,
FileStream,
Group,
coField,
} from "jazz-tools";
import { afterAll, afterEach, describe, expect, test } from "vitest";
import { AuthSecretStorage, FileStream, Group, co, z } from "jazz-tools";
import { assert, afterAll, afterEach, describe, expect, test } from "vitest";
import { createAccountContext, startSyncServer } from "./testUtils";
class TestMap extends CoMap {
value = coField.string;
}
const TestMAP = co.map({
value: z.string(),
});
class CustomAccount extends Account {
root = coField.ref(TestMap);
migrate() {
if (!this.root) {
this.root = TestMap.create({ value: "initial" }, { owner: this });
const CustomAccount = co
.account({
root: TestMAP,
profile: co.profile(),
})
.withMigration((me) => {
if (me.root === undefined) {
me.root = TestMAP.create({ value: "initial" }, { owner: me });
}
}
}
});
afterAll(async () => {
await commands.cleanup();
@@ -43,13 +37,13 @@ describe("Browser sync on unstable connection", () => {
AccountSchema: CustomAccount,
});
const bytes10MB = 1e7;
const bytes1MB = 1e6;
const group = Group.create();
group.addMember("everyone", "reader");
const promise = FileStream.createFromBlob(
new Blob(["1".repeat(bytes10MB)], { type: "image/png" }),
new Blob(["1".repeat(bytes1MB)], { type: "image/png" }),
group,
);
@@ -78,7 +72,63 @@ describe("Browser sync on unstable connection", () => {
const fileOnSecondAccount = await promise2;
expect(fileOnSecondAccount?.size).toBe(bytes10MB);
expect(fileOnSecondAccount?.size).toBe(bytes1MB);
});
test("wait for online when creating data when offline and calling waitForSync", async () => {
const syncServer = await startSyncServer();
const { contextManager } = await createAccountContext({
sync: {
peer: syncServer.url,
},
storage: "indexedDB",
AccountSchema: CustomAccount,
});
const group = Group.create();
group.addMember("everyone", "reader");
await syncServer.setOnline(false);
const map = TestMAP.create({ value: "initial" }, { owner: group });
await new Promise((resolve) => setTimeout(resolve, 10));
await syncServer.setOnline(true);
await map.waitForSync();
contextManager.done();
await new AuthSecretStorage().clear();
await new Promise((resolve) => {
const req = indexedDB.deleteDatabase("jazz-storage");
req.onsuccess = function () {
resolve(undefined);
};
});
await createAccountContext({
sync: {
peer: syncServer.url,
},
storage: "indexedDB",
AccountSchema: CustomAccount,
});
await syncServer.setOnline(false);
const promise = TestMAP.load(map.id);
await new Promise((resolve) => setTimeout(resolve, 10));
await syncServer.setOnline(true);
const mapOnSecondAccount = await promise;
assert(mapOnSecondAccount);
expect(mapOnSecondAccount.value).toBe("initial");
});
test("load files from storage correctly when pointing to different sync servers", async () => {