Compare commits
48 Commits
jazz-react
...
cojson-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937284f7e9 | ||
|
|
e999727c70 | ||
|
|
2197766624 | ||
|
|
3fe53a3a4a | ||
|
|
867cb6b7a5 | ||
|
|
0401fcf2a8 | ||
|
|
139a649279 | ||
|
|
9acccb5df2 | ||
|
|
fd90cdb49a | ||
|
|
df487d5335 | ||
|
|
1efe84c691 | ||
|
|
063553090e | ||
|
|
6dffe73bd2 | ||
|
|
68cb357a94 | ||
|
|
4f7bc91502 | ||
|
|
f61a120560 | ||
|
|
2f1307a0ba | ||
|
|
fa15ea56d1 | ||
|
|
1e58ecb3ac | ||
|
|
ceeabfaf89 | ||
|
|
70ce7c5736 | ||
|
|
6afdb16739 | ||
|
|
b0b2b85a6f | ||
|
|
28c19c134f | ||
|
|
0924c9baaa | ||
|
|
b2712e18a2 | ||
|
|
66894b63d7 | ||
|
|
b1a05143e3 | ||
|
|
fb761ce66d | ||
|
|
07a6c340dc | ||
|
|
0fea904dd0 | ||
|
|
373aef313f | ||
|
|
a584590ed8 | ||
|
|
0a830e29a9 | ||
|
|
efff4d0f4f | ||
|
|
ea2b01d8a2 | ||
|
|
55cb83e6e0 | ||
|
|
6290088fec | ||
|
|
b9c17b37db | ||
|
|
6c76ff8fbf | ||
|
|
3c6a2a6092 | ||
|
|
e8a950e61a | ||
|
|
e2cbf035de | ||
|
|
47599b6307 | ||
|
|
901d0762ee | ||
|
|
d1c1b0c5cc | ||
|
|
cf4ad7285d | ||
|
|
2983c7bd58 |
@@ -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"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
|
||||
## 0.0.101
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
|
||||
## 0.0.100
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-svelte",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
3
examples/server-worker-http/vercel.json
Normal file
3
examples/server-worker-http/vercel.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignoreCommand": "echo true"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -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>
|
||||
|
||||
|
||||
@@ -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---
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# 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
|
||||
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# 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
|
||||
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# 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
|
||||
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -165,6 +165,6 @@ export function createWebSocketPeer({
|
||||
incoming,
|
||||
outgoing,
|
||||
role,
|
||||
deletePeerStateOnClose,
|
||||
persistent: !deletePeerStateOnClose,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
## 0.15.11
|
||||
|
||||
## 0.15.10
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"libsql": "^0.5.13",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ export class LocalNode {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
removeStorage() {
|
||||
this.storage?.close();
|
||||
this.storage = undefined;
|
||||
}
|
||||
|
||||
getCoValue(id: RawCoID) {
|
||||
let entry = this.coValues.get(id);
|
||||
|
||||
@@ -348,12 +353,10 @@ export class LocalNode {
|
||||
skipLoadingFromPeer?: PeerID,
|
||||
skipRetry?: boolean,
|
||||
): Promise<CoValueCore> {
|
||||
if (!id) {
|
||||
throw new Error("Trying to load CoValue with undefined id");
|
||||
}
|
||||
|
||||
if (!id.startsWith("co_z")) {
|
||||
throw new Error(`Trying to load CoValue with invalid id ${id}`);
|
||||
if (typeof id !== "string" || !id.startsWith("co_z")) {
|
||||
throw new TypeError(
|
||||
`Trying to load CoValue with invalid id ${Array.isArray(id) ? JSON.stringify(id) : id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.crashed) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
@@ -54,6 +57,43 @@ describe("loading coValues from server", () => {
|
||||
`);
|
||||
});
|
||||
|
||||
test("coValue load throws on invalid id", async () => {
|
||||
const { node } = setupTestNode({
|
||||
connected: true,
|
||||
});
|
||||
|
||||
await expect(async () => await node.load("test" as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id test",
|
||||
);
|
||||
await expect(async () => await node.load(null as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id null",
|
||||
);
|
||||
await expect(async () => await node.load(undefined as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id undefined",
|
||||
);
|
||||
await expect(async () => await node.load(1 as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id 1",
|
||||
);
|
||||
await expect(async () => await node.load({} as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id [object Object]",
|
||||
);
|
||||
await expect(async () => await node.load([] as any)).rejects.toThrow(
|
||||
"Trying to load CoValue with invalid id []",
|
||||
);
|
||||
await expect(async () => await node.load(["test"] as any)).rejects.toThrow(
|
||||
'Trying to load CoValue with invalid id ["test"]',
|
||||
);
|
||||
await expect(
|
||||
async () => await node.load((() => {}) as any),
|
||||
).rejects.toMatchInlineSnapshot(`
|
||||
[TypeError: Trying to load CoValue with invalid id () => {
|
||||
}]
|
||||
`);
|
||||
await expect(
|
||||
async () => await node.load(new Date() as any),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("unavailable coValue retry with skipRetry set to true", async () => {
|
||||
const client = setupTestNode();
|
||||
const client2 = setupTestNode();
|
||||
@@ -470,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");
|
||||
@@ -480,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({
|
||||
@@ -496,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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -872,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -991,7 +993,7 @@ describe("LocalNode.load", () => {
|
||||
|
||||
// @ts-expect-error Testing with undefined ID
|
||||
await expect(client.node.load(undefined)).rejects.toThrow(
|
||||
"Trying to load CoValue with undefined id",
|
||||
"Trying to load CoValue with invalid id undefined",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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---
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
- jazz-betterauth-client-plugin@0.15.13
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
- jazz-betterauth-client-plugin@0.15.12
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-auth-betterauth",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# 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
|
||||
|
||||
- jazz-betterauth-server-plugin@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-betterauth-server-plugin@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-betterauth-client-plugin",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-betterauth-server-plugin",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
- jazz-auth-betterauth@0.15.13
|
||||
- jazz-betterauth-client-plugin@0.15.13
|
||||
- cojson@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
- jazz-auth-betterauth@0.15.12
|
||||
- jazz-betterauth-client-plugin@0.15.12
|
||||
- cojson@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jazz-react-auth-betterauth",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.tsx",
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [6c76ff8]
|
||||
- jazz-tools@0.15.13
|
||||
- cojson@0.15.13
|
||||
- cojson-storage-sqlite@0.15.13
|
||||
- cojson-transport-ws@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d1c1b0c]
|
||||
- Updated dependencies [cf4ad72]
|
||||
- jazz-tools@0.15.12
|
||||
- cojson@0.15.12
|
||||
- cojson-storage-sqlite@0.15.12
|
||||
- cojson-transport-ws@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"bin": "./dist/index.js",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.15.11",
|
||||
"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.11",
|
||||
"cojson-storage-sqlite": "workspace:0.15.11",
|
||||
"cojson-transport-ws": "workspace:0.15.11",
|
||||
"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.11",
|
||||
"jazz-tools": "workspace:0.15.15",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# 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
|
||||
|
||||
- 6c76ff8: Fix load failures when loading a missing ref declared with z.optional and Schema.optional
|
||||
- cojson@0.15.13
|
||||
- cojson-storage-indexeddb@0.15.13
|
||||
- cojson-transport-ws@0.15.13
|
||||
|
||||
## 0.15.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d1c1b0c: Fix stuck authentication when using onAnonymousAccountDiscarded with a storage
|
||||
- cf4ad72: fix unhandled rejection on CoValue.load
|
||||
- cojson@0.15.12
|
||||
- cojson-storage-indexeddb@0.15.12
|
||||
- cojson-transport-ws@0.15.12
|
||||
|
||||
## 0.15.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.15.11",
|
||||
"version": "0.15.15",
|
||||
"dependencies": {
|
||||
"@manuscripts/prosemirror-recreate-steps": "^0.1.4",
|
||||
"@scure/base": "1.2.1",
|
||||
|
||||
@@ -47,6 +47,26 @@ describe("useCoState", () => {
|
||||
expect(result.current?.value).toBe("123");
|
||||
});
|
||||
|
||||
it("should return null on invalid id", async () => {
|
||||
const TestMap = co.map({
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const account = await createJazzTestAccount({
|
||||
isCurrentActiveAccount: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCoState(TestMap, "test", {}), {
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update the value when the coValue changes", async () => {
|
||||
const TestMap = co.map({
|
||||
value: z.string(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,6 +35,7 @@ export type {
|
||||
TextPos,
|
||||
AccountClass,
|
||||
AccountCreationProps,
|
||||
WithHelpers,
|
||||
} from "./internal.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -279,8 +279,12 @@ export class JazzContextManager<
|
||||
},
|
||||
);
|
||||
|
||||
prevContext.node.syncManager.addPeer(currentAccountAsPeer);
|
||||
// Closing storage on the prevContext to avoid conflicting transactions and getting stuck on waitForAllCoValuesSync
|
||||
// The storage is reachable through currentContext using the connectedPeers
|
||||
prevContext.node.removeStorage();
|
||||
|
||||
currentContext.node.syncManager.addPeer(prevAccountAsPeer);
|
||||
prevContext.node.syncManager.addPeer(currentAccountAsPeer);
|
||||
|
||||
try {
|
||||
await this.props.onAnonymousAccountDiscarded?.(prevContext.me);
|
||||
|
||||
@@ -43,27 +43,35 @@ type SchemaField =
|
||||
| z.core.$ZodCatch<z.core.$ZodType>
|
||||
| (z.core.$ZodCustom<any, any> & { builtin: any });
|
||||
|
||||
export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
export function schemaFieldToCoFieldDef(
|
||||
schema: SchemaField,
|
||||
isOptional = false,
|
||||
) {
|
||||
if (isCoValueClass(schema)) {
|
||||
return coField.ref(schema);
|
||||
if (isOptional) {
|
||||
return coField.ref(schema, { optional: true });
|
||||
} else {
|
||||
return coField.ref(schema);
|
||||
}
|
||||
} else if (isCoValueSchema(schema)) {
|
||||
if (isAnyCoOptionalSchema(schema)) {
|
||||
return coField.ref(schema.getCoValueClass(), {
|
||||
optional: true,
|
||||
});
|
||||
}
|
||||
return coField.ref(schema.getCoValueClass());
|
||||
|
||||
if (isOptional) {
|
||||
return coField.ref(schema.getCoValueClass(), { optional: true });
|
||||
} else {
|
||||
return coField.ref(schema.getCoValueClass());
|
||||
}
|
||||
} else {
|
||||
if ("_zod" in schema) {
|
||||
if (schema._zod.def.type === "optional") {
|
||||
const inner = zodSchemaToCoSchemaOrKeepPrimitive(
|
||||
schema._zod.def.innerType,
|
||||
);
|
||||
if (isCoValueClass(inner)) {
|
||||
return coField.ref(inner, { optional: true });
|
||||
} else {
|
||||
return schemaFieldToCoFieldDef(inner);
|
||||
}
|
||||
return schemaFieldToCoFieldDef(inner, true);
|
||||
} else if (schema._zod.def.type === "string") {
|
||||
return coField.string;
|
||||
} else if (schema._zod.def.type === "number") {
|
||||
@@ -77,6 +85,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
} else if (schema._zod.def.type === "readonly") {
|
||||
return schemaFieldToCoFieldDef(
|
||||
(schema as unknown as ZodReadonly).def.innerType as SchemaField,
|
||||
isOptional,
|
||||
);
|
||||
} else if (schema._zod.def.type === "date") {
|
||||
return coField.optional.Date;
|
||||
@@ -86,6 +95,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
// Mostly to support z.json()
|
||||
return schemaFieldToCoFieldDef(
|
||||
(schema as unknown as ZodLazy).unwrap() as SchemaField,
|
||||
isOptional,
|
||||
);
|
||||
} else if (
|
||||
schema._zod.def.type === "default" ||
|
||||
@@ -98,6 +108,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
return schemaFieldToCoFieldDef(
|
||||
(schema as unknown as ZodDefault | ZodCatch).def
|
||||
.innerType as SchemaField,
|
||||
isOptional,
|
||||
);
|
||||
} else if (schema._zod.def.type === "literal") {
|
||||
if (
|
||||
@@ -129,7 +140,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
return coField.json();
|
||||
} else if (schema._zod.def.type === "custom") {
|
||||
if ("builtin" in schema) {
|
||||
return schemaFieldToCoFieldDef(schema.builtin);
|
||||
return schemaFieldToCoFieldDef(schema.builtin, isOptional);
|
||||
} else {
|
||||
throw new Error(`Unsupported custom zod type`);
|
||||
}
|
||||
@@ -137,9 +148,13 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
|
||||
if (isUnionOfPrimitivesDeeply(schema)) {
|
||||
return coField.json();
|
||||
} else if (isUnionOfCoMapsDeeply(schema)) {
|
||||
return coField.ref<CoValueClass<CoMap>>(
|
||||
schemaUnionDiscriminatorFor(schema),
|
||||
);
|
||||
const result = schemaUnionDiscriminatorFor(schema);
|
||||
|
||||
if (isOptional) {
|
||||
return coField.ref<CoValueClass<CoMap>>(result, { optional: true });
|
||||
} else {
|
||||
return coField.ref<CoValueClass<CoMap>>(result);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"z.union()/z.discriminatedUnion() of mixed collaborative and non-collaborative types is not supported",
|
||||
|
||||
@@ -28,6 +28,10 @@ export class CoValueCoreSubscription {
|
||||
this.subscribeToState();
|
||||
this.listener("unavailable");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Unexpected error loading CoValue: ", error);
|
||||
this.listener("unavailable");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StorageAPI } from "cojson";
|
||||
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
getPeerConnectedToTestSyncServer,
|
||||
setupJazzTestSync,
|
||||
} from "../testing";
|
||||
import { createAsyncStorage, getDbPath } from "./testStorage";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -40,12 +42,14 @@ class TestJazzContextManager<Acc extends Account> extends JazzContextManager<
|
||||
JazzContextManagerBaseProps<Acc> & {
|
||||
defaultProfileName?: string;
|
||||
AccountSchema?: AccountClass<Acc>;
|
||||
storage?: string;
|
||||
}
|
||||
> {
|
||||
async getNewContext(
|
||||
props: JazzContextManagerBaseProps<Acc> & {
|
||||
defaultProfileName?: string;
|
||||
AccountSchema?: AccountClass<Acc> & CoValueFromRaw<Acc>;
|
||||
storage?: string;
|
||||
},
|
||||
authProps?: JazzContextManagerAuthProps,
|
||||
) {
|
||||
@@ -58,6 +62,7 @@ class TestJazzContextManager<Acc extends Account> extends JazzContextManager<
|
||||
sessionProvider: randomSessionProvider,
|
||||
authSecretStorage: this.getAuthSecretStorage(),
|
||||
AccountSchema: props.AccountSchema,
|
||||
storage: await createAsyncStorage({ filename: props.storage }),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -76,12 +81,14 @@ class TestJazzContextManager<Acc extends Account> extends JazzContextManager<
|
||||
describe("ContextManager", () => {
|
||||
let manager: TestJazzContextManager<Account>;
|
||||
let authSecretStorage: AuthSecretStorage;
|
||||
let storage: StorageAPI;
|
||||
|
||||
function getCurrentValue() {
|
||||
return manager.getCurrentValue() as JazzAuthContext<Account>;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
storage = await createAsyncStorage({});
|
||||
KvStoreContext.getInstance().initialize(new InMemoryKVStore());
|
||||
authSecretStorage = new AuthSecretStorage();
|
||||
await authSecretStorage.clear();
|
||||
@@ -232,6 +239,78 @@ describe("ContextManager", () => {
|
||||
expect(onAnonymousAccountDiscarded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("onAnonymousAccountDiscarded should not block the authentication when storage is active", async () => {
|
||||
const dbFilename = getDbPath();
|
||||
|
||||
const AccountRoot = co.map({
|
||||
value: z.string(),
|
||||
get transferredRoot(): z.ZodOptional<typeof AccountRoot> {
|
||||
return co.optional(AccountRoot);
|
||||
},
|
||||
});
|
||||
|
||||
let lastRootId: string | undefined;
|
||||
|
||||
const CustomAccount = co
|
||||
.account({
|
||||
root: AccountRoot,
|
||||
profile: co.profile(),
|
||||
})
|
||||
.withMigration(async (account) => {
|
||||
account.root = AccountRoot.create(
|
||||
{
|
||||
value: "Hello",
|
||||
},
|
||||
Group.create(this).makePublic(),
|
||||
);
|
||||
});
|
||||
|
||||
const customManager = new TestJazzContextManager<
|
||||
InstanceOfSchema<typeof CustomAccount>
|
||||
>();
|
||||
|
||||
await customManager.createContext({
|
||||
AccountSchema: anySchemaToCoSchema(CustomAccount),
|
||||
storage: dbFilename,
|
||||
onAnonymousAccountDiscarded: async (anonymousAccount) => {
|
||||
const anonymousAccountWithRoot = await anonymousAccount.ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
const me = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
me.root.transferredRoot = anonymousAccountWithRoot.root;
|
||||
},
|
||||
});
|
||||
|
||||
const prevContextNode = customManager.getCurrentValue()!.node;
|
||||
|
||||
expect(prevContextNode.storage).toBeDefined();
|
||||
|
||||
const account = (
|
||||
customManager.getCurrentValue() as JazzAuthContext<
|
||||
InstanceOfSchema<typeof CustomAccount>
|
||||
>
|
||||
).me;
|
||||
|
||||
await customManager.authenticate({
|
||||
accountID: account.id,
|
||||
accountSecret: account._raw.core.node.getCurrentAgent().agentSecret,
|
||||
provider: "test",
|
||||
});
|
||||
|
||||
// The storage should be closed and set to undefined
|
||||
expect(prevContextNode.storage).toBeUndefined();
|
||||
|
||||
const me = await CustomAccount.getMe().ensureLoaded({
|
||||
resolve: { root: { transferredRoot: true } },
|
||||
});
|
||||
|
||||
expect(me.root.transferredRoot?.value).toBe("Hello");
|
||||
});
|
||||
|
||||
test("the migration should be applied correctly on existing accounts", async () => {
|
||||
const AccountRoot = co.map({
|
||||
value: z.string(),
|
||||
@@ -266,8 +345,6 @@ describe("ContextManager", () => {
|
||||
>
|
||||
).me;
|
||||
|
||||
console.log("before", account._refs.root?.id);
|
||||
|
||||
await customManager.authenticate({
|
||||
accountID: account.id,
|
||||
accountSecret: account._raw.core.node.getCurrentAgent().agentSecret,
|
||||
@@ -278,8 +355,6 @@ describe("ContextManager", () => {
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
console.log("after", me._refs.root?.id);
|
||||
|
||||
expect(me.root.id).toBe(lastRootId);
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -33,6 +33,129 @@ test("load a value", async () => {
|
||||
expect(john?.name).toBe("John");
|
||||
});
|
||||
|
||||
test("return null if id is invalid", async () => {
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const john = await Person.load("test");
|
||||
expect(john).toBeNull();
|
||||
});
|
||||
|
||||
test("load a missing optional value (co.optional)", async () => {
|
||||
const Dog = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
dog: co.optional(Dog),
|
||||
});
|
||||
|
||||
const group = Group.create();
|
||||
const map = Person.create({ name: "John" }, group);
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
const alice = await createJazzTestAccount();
|
||||
|
||||
const john = await Person.load(map.id, {
|
||||
loadAs: alice,
|
||||
resolve: { dog: true },
|
||||
});
|
||||
|
||||
assert(john);
|
||||
|
||||
expect(john.name).toBe("John");
|
||||
expect(john.dog).toBeUndefined();
|
||||
});
|
||||
|
||||
test("load a missing optional value (z.optional)", async () => {
|
||||
const Dog = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
dog: z.optional(Dog),
|
||||
});
|
||||
|
||||
const group = Group.create();
|
||||
const map = Person.create({ name: "John" }, group);
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
const alice = await createJazzTestAccount();
|
||||
|
||||
const john = await Person.load(map.id, {
|
||||
loadAs: alice,
|
||||
resolve: { dog: true },
|
||||
});
|
||||
|
||||
assert(john);
|
||||
|
||||
expect(john.name).toBe("John");
|
||||
expect(john.dog).toBeUndefined();
|
||||
});
|
||||
|
||||
test("load a missing optional value (Schema.optional)", async () => {
|
||||
const Dog = co.map({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
dog: Dog.optional(),
|
||||
});
|
||||
|
||||
const group = Group.create();
|
||||
const map = Person.create({ name: "John" }, group);
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
const alice = await createJazzTestAccount();
|
||||
|
||||
const john = await Person.load(map.id, {
|
||||
loadAs: alice,
|
||||
resolve: { dog: true },
|
||||
});
|
||||
|
||||
assert(john);
|
||||
|
||||
expect(john.name).toBe("John");
|
||||
expect(john.dog).toBeUndefined();
|
||||
});
|
||||
|
||||
test("load a missing optional value (optional discrminatedUnion)", async () => {
|
||||
const Dog = co.map({
|
||||
type: z.literal("dog"),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const Cat = co.map({
|
||||
type: z.literal("cat"),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
pet: co.discriminatedUnion("type", [Dog, Cat]).optional(),
|
||||
});
|
||||
|
||||
const group = Group.create();
|
||||
const map = Person.create({ name: "John" }, group);
|
||||
group.addMember("everyone", "reader");
|
||||
|
||||
const alice = await createJazzTestAccount();
|
||||
|
||||
const john = await Person.load(map.id, {
|
||||
loadAs: alice,
|
||||
resolve: { pet: true },
|
||||
});
|
||||
|
||||
assert(john);
|
||||
|
||||
expect(john.name).toBe("John");
|
||||
expect(john.pet).toBeUndefined();
|
||||
});
|
||||
|
||||
test("retry an unavailable value", async () => {
|
||||
const Person = co.map({
|
||||
name: z.string(),
|
||||
|
||||
70
packages/jazz-tools/src/tools/tests/testStorage.ts
Normal file
70
packages/jazz-tools/src/tools/tests/testStorage.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { SQLiteDatabaseDriverAsync, getSqliteStorageAsync } from "cojson";
|
||||
import Database, { type Database as DatabaseT } from "libsql";
|
||||
import { onTestFinished } from "vitest";
|
||||
|
||||
class LibSQLSqliteAsyncDriver implements SQLiteDatabaseDriverAsync {
|
||||
private readonly db: DatabaseT;
|
||||
|
||||
constructor(filename: string) {
|
||||
this.db = new Database(filename, {});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.db.pragma("journal_mode = WAL");
|
||||
}
|
||||
|
||||
async run(sql: string, params: unknown[]) {
|
||||
this.db.prepare(sql).run(params);
|
||||
}
|
||||
|
||||
async query<T>(sql: string, params: unknown[]): Promise<T[]> {
|
||||
return this.db.prepare(sql).all(params) as T[];
|
||||
}
|
||||
|
||||
async get<T>(sql: string, params: unknown[]): Promise<T | undefined> {
|
||||
return this.db.prepare(sql).get(params) as T | undefined;
|
||||
}
|
||||
|
||||
async transaction(callback: () => unknown) {
|
||||
await this.run("BEGIN TRANSACTION", []);
|
||||
|
||||
try {
|
||||
await callback();
|
||||
await this.run("COMMIT", []);
|
||||
} catch (error) {
|
||||
await this.run("ROLLBACK", []);
|
||||
}
|
||||
}
|
||||
|
||||
async closeDb() {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAsyncStorage({ filename }: { filename?: string }) {
|
||||
const storage = await getSqliteStorageAsync(
|
||||
new LibSQLSqliteAsyncDriver(getDbPath(filename)),
|
||||
);
|
||||
|
||||
onTestFinished(() => {
|
||||
storage.close();
|
||||
});
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export function getDbPath(defaultDbPath?: string) {
|
||||
const dbPath = defaultDbPath ?? join(tmpdir(), `test-${randomUUID()}.db`);
|
||||
|
||||
if (!defaultDbPath) {
|
||||
onTestFinished(() => {
|
||||
unlinkSync(dbPath);
|
||||
});
|
||||
}
|
||||
|
||||
return dbPath;
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { defineProject } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
export default defineProject({
|
||||
test: {
|
||||
workspace: [
|
||||
{
|
||||
test: {
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
checker: "tsc",
|
||||
},
|
||||
include: ["src/**/*.test.ts"],
|
||||
name: "unit",
|
||||
},
|
||||
},
|
||||
name: "jazz-tools",
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
checker: "tsc",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
include: ["src/**/*.test.browser.ts"],
|
||||
name: "browser",
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: "playwright",
|
||||
@@ -24,6 +18,13 @@ export default defineConfig({
|
||||
screenshotFailures: false,
|
||||
instances: [{ browser: "chromium" }],
|
||||
},
|
||||
name: "browser",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
include: ["src/**/*.test.{js,ts,svelte}"],
|
||||
name: "unit",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
1
packages/quint-ui/.gitignore
vendored
Normal file
1
packages/quint-ui/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
next-env.d.ts
|
||||
7
packages/quint-ui/CHANGELOG.md
Normal file
7
packages/quint-ui/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# quint-ui
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f61a120: Initial Quint-UI release
|
||||
69
packages/quint-ui/README.md
Normal file
69
packages/quint-ui/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Quint UI
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add quint-ui tailwindcss
|
||||
```
|
||||
|
||||
then in your tailwind styles (ie. `styles.css`) add:
|
||||
```css
|
||||
@import "quint-ui/styles.css";
|
||||
@source "../node_modules/quint-ui";
|
||||
```
|
||||
> [!TIP]
|
||||
> There's no need to do
|
||||
> ```css
|
||||
> @import "tailwindcss";
|
||||
> ```
|
||||
> Quint does this for you.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import { Button } from "quint-ui";
|
||||
|
||||
<Button>Click me</Button>
|
||||
```
|
||||
|
||||
## Development workflow
|
||||
|
||||
### In Quint
|
||||
|
||||
In this directory, run:
|
||||
```sh
|
||||
pnpm dev:docs
|
||||
```
|
||||
This starts a nextjs app you can use to write docs and/or test components.
|
||||
|
||||
### In example apps
|
||||
|
||||
In this directory, run:
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
This starts typescript in watch mode.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Make sure quint-ui is installed in the example via
|
||||
> ```json
|
||||
> "dependencies": {
|
||||
> "quint-ui": "workspace:*"
|
||||
> }
|
||||
> ```
|
||||
|
||||
## Building
|
||||
|
||||
### Docs website
|
||||
|
||||
```sh
|
||||
pnpm build:docs
|
||||
```
|
||||
|
||||
### Package
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
25
packages/quint-ui/app/docs/button/page.tsx
Normal file
25
packages/quint-ui/app/docs/button/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Button } from "@/src/components/button";
|
||||
|
||||
export default function ButtonDocsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Button</h1>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button intent="primary">Default</Button>
|
||||
<Button intent="primary" variant="outline">
|
||||
Outline
|
||||
</Button>
|
||||
<Button intent="primary" variant="ghost">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button intent="primary" variant="link">
|
||||
Link
|
||||
</Button>
|
||||
<Button intent="primary" variant="inverted">
|
||||
Inverted
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
packages/quint-ui/app/docs/page.tsx
Normal file
14
packages/quint-ui/app/docs/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Components</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/docs/button">Button</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
packages/quint-ui/app/layout.tsx
Normal file
18
packages/quint-ui/app/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import "@/src/globals.css";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Quint UI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div className="container mx-auto">{children}</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
22
packages/quint-ui/app/page.tsx
Normal file
22
packages/quint-ui/app/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Button } from "@/src/components/button";
|
||||
import { BookIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex flex-col gap-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Quint UI</h1>
|
||||
|
||||
{/* Example usage of the button component with an icon and rendered as a nextjs link */}
|
||||
<Button
|
||||
variant="default"
|
||||
intent="primary"
|
||||
size="lg"
|
||||
render={<Link href="/docs" />}
|
||||
>
|
||||
<BookIcon />
|
||||
Read the docs!
|
||||
</Button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
6
packages/quint-ui/next.config.ts
Normal file
6
packages/quint-ui/next.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
export default {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
};
|
||||
44
packages/quint-ui/package.json
Normal file
44
packages/quint-ui/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "quint-ui",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./styles.css": "./src/globals.css"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "pnpm clean && tsc --project tsconfig.build.json --watch",
|
||||
"dev:website": "next dev --turbopack",
|
||||
"build": "pnpm clean && tsc --project tsconfig.build.json",
|
||||
"build:website": "next build",
|
||||
"clean": "rm -rf ./dist",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "1.0.0-beta.1",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tw-animate-css": "^1.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.0.0",
|
||||
"react-dom": ">=19.0.0",
|
||||
"tailwindcss": ">=4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"next": "15.4.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"lucide-react": "^0.525.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
6
packages/quint-ui/postcss.config.mjs
Normal file
6
packages/quint-ui/postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
160
packages/quint-ui/src/components/button.tsx
Normal file
160
packages/quint-ui/src/components/button.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { mergeProps } from "@base-ui-components/react/merge-props";
|
||||
import { useRender } from "@base-ui-components/react/use-render";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
const button = tv({
|
||||
base: "inline-flex items-center justify-center gap-2 rounded-sm text-center transition-colors w-fit text-nowrap disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed disabled:pointer-events-none font-medium",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"focus:outline-none focus-visible:ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-opacity-10",
|
||||
ghost: "",
|
||||
outline: "shadow-sm border",
|
||||
link: "",
|
||||
inverted: "",
|
||||
},
|
||||
intent: {
|
||||
default: "border-secondary",
|
||||
primary: "border-primary",
|
||||
success: "border-success",
|
||||
danger: "border-danger",
|
||||
},
|
||||
size: {
|
||||
sm: "text-sm py-1 px-2 [&>svg]:size-4",
|
||||
md: "py-1.5 px-3 h-[36px] [&>svg]:size-4",
|
||||
lg: "py-2 px-5 md:px-6 md:py-2.5 [&>svg]:size-5",
|
||||
icon: "p-2 [&>svg]:size-4",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
// Default Variants
|
||||
{
|
||||
variant: "default",
|
||||
intent: ["primary", "success", "danger", "default"],
|
||||
className: "bg-gradient-to-t from-7% via-50% to-95%",
|
||||
},
|
||||
{
|
||||
variant: "default",
|
||||
intent: "primary",
|
||||
className: [
|
||||
"text-primary-foreground",
|
||||
"from-primary-dark via-primary to-primary-light",
|
||||
"hover:from-primary-brightLight hover:to-primary-light",
|
||||
"active:from-primary-brightDark active:to-primary-light active:border-primary-transparent ",
|
||||
"focus:ring-primary focus:border-primary-transparent",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "default",
|
||||
intent: "success",
|
||||
className: [
|
||||
"text-success-foreground",
|
||||
"from-success-dark via-success to-success-light",
|
||||
"hover:from-success-brightLight hover:to-success-light",
|
||||
"active:from-success-brightDark active:to-success-light",
|
||||
"focus:ring-success",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "default",
|
||||
intent: "danger",
|
||||
className: [
|
||||
"text-danger-foreground",
|
||||
"from-danger-dark via-danger to-danger-light",
|
||||
"hover:from-danger-brightLight hover:to-danger-light",
|
||||
"active:from-danger-brightDark active:to-danger-light",
|
||||
"focus:ring-danger",
|
||||
],
|
||||
},
|
||||
// Outline Variants
|
||||
{
|
||||
variant: "outline",
|
||||
intent: "primary",
|
||||
className: [
|
||||
"text-primary",
|
||||
"active:bg-primary/30",
|
||||
"hover:text-primary-light hover:border-primary-light",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "outline",
|
||||
intent: "danger",
|
||||
className: [
|
||||
"text-danger",
|
||||
"active:bg-danger/30",
|
||||
"hover:text-danger-light hover:border-danger-light",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "outline",
|
||||
intent: "success",
|
||||
className: [
|
||||
"text-success-dark",
|
||||
"active:bg-success/30",
|
||||
"hover:text-success-light hover:border-success-light",
|
||||
],
|
||||
},
|
||||
// Ghost Variants
|
||||
{
|
||||
variant: "ghost",
|
||||
intent: "primary",
|
||||
className: [
|
||||
"text-primary",
|
||||
"hover:bg-primary/10",
|
||||
"active:bg-primary/30",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "ghost",
|
||||
intent: "primary",
|
||||
className: [
|
||||
"text-primary",
|
||||
"hover:bg-primary/10",
|
||||
"active:bg-primary/30",
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: "ghost",
|
||||
intent: "danger",
|
||||
className: ["text-danger", "hover:bg-danger/10", "active:bg-danger/30"],
|
||||
},
|
||||
{
|
||||
variant: "ghost",
|
||||
intent: "success",
|
||||
className: [
|
||||
"text-success",
|
||||
"hover:bg-success/10",
|
||||
"active:bg-success/30",
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
variant: "default",
|
||||
intent: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type ButtonVariants = VariantProps<typeof button>;
|
||||
|
||||
interface ButtonProps
|
||||
extends Omit<useRender.ComponentProps<"button">, keyof ButtonVariants>,
|
||||
ButtonVariants {}
|
||||
|
||||
export function Button({ render = <button />, ...props }: ButtonProps) {
|
||||
const element = useRender({
|
||||
render,
|
||||
props: mergeProps<"button">(
|
||||
{
|
||||
className: button({
|
||||
size: props.size,
|
||||
variant: props.variant,
|
||||
intent: props.intent,
|
||||
}),
|
||||
},
|
||||
props,
|
||||
),
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
152
packages/quint-ui/src/globals.css
Normal file
152
packages/quint-ui/src/globals.css
Normal file
@@ -0,0 +1,152 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* Dark mode. If you want to use dark mode, you can add the dark class to the body/head element. */
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary-transparent: var(--primary-transparent);
|
||||
--color-primary-dark: var(--primary-dark);
|
||||
--color-primary-light: var(--primary-light);
|
||||
--color-primary-brightLight: var(--primary-brightLight);
|
||||
--color-primary-brightDark: var(--primary-brightDark);
|
||||
|
||||
--color-alert: var(--alert);
|
||||
--color-alert-foreground: var(--alert-foreground);
|
||||
--color-alert-transparent: var(--alert-transparent);
|
||||
--color-alert-dark: var(--alert-dark);
|
||||
--color-alert-light: var(--alert-light);
|
||||
--color-alert-brightLight: var(--alert-brightLight);
|
||||
--color-alert-brightDark: var(--alert-brightDark);
|
||||
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-success-transparent: var(--success-transparent);
|
||||
--color-success-dark: var(--success-dark);
|
||||
--color-success-light: var(--success-light);
|
||||
--color-success-brightLight: var(--success-brightLight);
|
||||
--color-success-brightDark: var(--success-brightDark);
|
||||
|
||||
--color-info: var(--info);
|
||||
--color-info-foreground: var(--info-foreground);
|
||||
--color-info-transparent: var(--info-transparent);
|
||||
--color-info-dark: var(--info-dark);
|
||||
--color-info-light: var(--info-light);
|
||||
--color-info-brightLight: var(--info-brightLight);
|
||||
--color-info-brightDark: var(--info-brightDark);
|
||||
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-warning-transparent: var(--warning-transparent);
|
||||
--color-warning-light: var(--warning-light);
|
||||
--color-warning-dark: var(--warning-dark);
|
||||
--color-warning-brightLight: var(--warning-brightLight);
|
||||
--color-warning-brightDark: var(--warning-brightDark);
|
||||
|
||||
--color-tip: var(--tip);
|
||||
--color-tip-foreground: var(--tip-foreground);
|
||||
--color-tip-transparent: var(--tip-transparent);
|
||||
--color-tip-dark: var(--tip-dark);
|
||||
--color-tip-light: var(--tip-light);
|
||||
--color-tip-brightLight: var(--tip-brightLight);
|
||||
--color-tip-brightDark: var(--tip-brightDark);
|
||||
|
||||
--color-danger: var(--danger);
|
||||
--color-danger-foreground: var(--danger-foreground);
|
||||
--color-danger-transparent: var(--danger-transparent);
|
||||
--color-danger-dark: var(--danger-dark);
|
||||
--color-danger-light: var(--danger-light);
|
||||
--color-danger-brightLight: var(--danger-brightLight);
|
||||
--color-danger-brightDark: var(--danger-brightDark);
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: oklch(57.29% 0.235 261.2);
|
||||
--primary-transparent: lch(from var(--color-primary) l c h / 0.1);
|
||||
--primary-dark: lch(
|
||||
from var(--color-primary) calc(l - 10) calc(c - 1) calc(h + 5)
|
||||
);
|
||||
--primary-light: lch(
|
||||
from var(--color-primary) calc(l + 10) calc(c + 1) calc(h - 5)
|
||||
);
|
||||
--primary-brightLight: lch(
|
||||
from var(--color-primary) calc(l - 1) calc(c + 20) calc(h + 5)
|
||||
);
|
||||
--primary-brightDark: lch(
|
||||
from var(--color-primary) calc(l - 6) calc(c + 20) calc(h + 5)
|
||||
);
|
||||
--primary-foreground: oklch(0.985 0.045 264.32);
|
||||
|
||||
--alert: oklch(80.54% 0.169 76.45);
|
||||
--alert-transparent: oklch(0.843 0.222 85.04 / 0.3);
|
||||
--alert-dark: oklch(0.773 0.212 90.04);
|
||||
--alert-light: oklch(0.883 0.232 80.04);
|
||||
--alert-brightLight: oklch(0.833 0.522 100.04);
|
||||
--alert-brightDark: oklch(0.803 0.522 100.04);
|
||||
--alert-foreground: oklch(0.28 0.07 46);
|
||||
|
||||
--success: oklch(70.49% 0.16 150.62);
|
||||
--success-transparent: lch(from var(--color-success) l c h / 0.3);
|
||||
--success-dark: lch(
|
||||
from var(--color-success) calc(l - 7) calc(c - 1) calc(h + 5)
|
||||
);
|
||||
--success-light: lch(
|
||||
from var(--color-success) calc(l + 4) calc(c + 1) calc(h - 5)
|
||||
);
|
||||
--success-brightLight: lch(
|
||||
from var(--color-success) calc(l - 1) calc(c + 20) calc(h + 10)
|
||||
);
|
||||
--success-brightDark: lch(
|
||||
from var(--color-success) calc(l - 6) calc(c + 20) calc(h + 10)
|
||||
);
|
||||
--success-foreground: oklch(0.28 0.07 46);
|
||||
|
||||
--info: oklch(61.13% 0.248 312.2);
|
||||
--info-transparent: oklch(0.514 0.247 291.21 / 0.3);
|
||||
--info-dark: oklch(0.444 0.237 296.21);
|
||||
--info-light: oklch(0.554 0.257 286.21);
|
||||
--info-brightLight: oklch(0.504 0.447 296.21);
|
||||
--info-brightDark: oklch(0.474 0.447 296.21);
|
||||
--info-foreground: oklch(0.28 0.07 46);
|
||||
|
||||
--warning: oklch(68.92% 0.206 39.71);
|
||||
--warning-transparent: oklch(0.661 0.372 75.45 / 0.3);
|
||||
--warning-dark: oklch(0.591 0.362 80.45);
|
||||
--warning-light: oklch(0.701 0.382 70.45);
|
||||
--warning-brightLight: oklch(0.651 0.672 90.45);
|
||||
--warning-brightDark: oklch(0.621 0.672 90.45);
|
||||
--warning-foreground: oklch(0.28 0.07 46);
|
||||
|
||||
--tip: oklch(76.08% 0.122 194.9);
|
||||
--tip-transparent: oklch(0.595 0.317 176.3 / 0.3);
|
||||
--tip-dark: oklch(0.525 0.307 181.3);
|
||||
--tip-light: oklch(0.635 0.327 171.3);
|
||||
--tip-brightLight: oklch(0.585 0.617 186.3);
|
||||
--tip-brightDark: oklch(0.555 0.617 186.3);
|
||||
--tip-foreground: oklch(0.28 0.07 46);
|
||||
|
||||
--danger: oklch(64.11% 0.202 23.95);
|
||||
--danger-transparent: lch(from var(--color-danger) l c h / 0.3);
|
||||
--danger-dark: lch(
|
||||
from var(--color-danger) calc(l - 7) calc(c - 1) calc(h + 5)
|
||||
);
|
||||
--danger-light: lch(
|
||||
from var(--color-danger) calc(l + 4) calc(c + 1) calc(h - 5)
|
||||
);
|
||||
--danger-brightLight: lch(
|
||||
from var(--color-danger) calc(l - 2) calc(c + 20) calc(h + 10)
|
||||
);
|
||||
--danger-brightDark: lch(
|
||||
from var(--color-danger) calc(l - 6) calc(c + 10) calc(h + 10)
|
||||
);
|
||||
--danger-foreground: oklch(0.28 0.07 46);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/*
|
||||
* Dark mode styles.
|
||||
* Here you can override the css variables above for dark mode.
|
||||
*/
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user