Compare commits

..

9 Commits

Author SHA1 Message Date
Guido D'Orsi
8fb1748433 Merge pull request #2750 from garden-co/changeset-release/main
Version Packages
2025-08-15 20:36:35 +02:00
github-actions[bot]
c8644bf678 Version Packages 2025-08-15 16:30:37 +00:00
Guido D'Orsi
269ee94338 test: skip flaky e2e test 2025-08-15 18:26:41 +02:00
Guido D'Orsi
dae80eeba8 Merge pull request #2751 from garden-co/feat/unmount
fix: remove unnecessary content sent as dependency
2025-08-15 18:25:51 +02:00
Guido D'Orsi
ce54667b4d Merge pull request #2752 from garden-co/more-unique-static-methods
Implement/expose loadUnique and upsertUnique on co.list and co.record
2025-08-15 18:25:33 +02:00
Anselm
5963658e28 Implement/expose loadUnique and upsertUnique on co.list and co.record 2025-08-15 17:20:48 +01:00
Guido D'Orsi
71c1411bbd fix: remove unnecessary content sent as dependency 2025-08-15 18:05:42 +02:00
Guido D'Orsi
71b221dc79 Merge pull request #2749 from garden-co/feat/unmount
feat: make the unmount function detach the CoValue from the localNode
2025-08-15 17:48:01 +02:00
Guido D'Orsi
2d11d448dc feat: make the unmount function detach the CoValue from the localNode 2025-08-15 17:41:18 +02:00
44 changed files with 832 additions and 48 deletions

View File

@@ -1,5 +1,12 @@
# passkey-svelte
## 0.0.118
### Patch Changes
- Updated dependencies [5963658]
- jazz-tools@0.17.5
## 0.0.117
### Patch Changes

View File

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

View File

@@ -1,7 +1,8 @@
import { clerk } from "@clerk/testing/playwright";
import { expect, test } from "@playwright/test";
test("login & expiration", async ({ page, context }) => {
// Flaky on CI
test.skip("login & expiration", async ({ page, context }) => {
// Clear cookies first
await context.clearCookies();

View File

@@ -1,5 +1,13 @@
# cojson-storage-indexeddb
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,13 @@
# cojson-transport-nodejs-ws
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,12 @@
# cojson
## 0.17.5
### Patch Changes
- 71c1411: Removed some unnecessary content messages sent after a local transaction when sending a value as dependency before the ack response
- 2d11d44: Make the CoValueCore.unmount function detach the CoValue from LocalNode
## 0.17.4
## 0.17.3

View File

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

View File

@@ -33,11 +33,7 @@ export class GarbageCollector {
const timeSinceLastAccessed = currentTime - verified.lastAccessed;
if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
const unmounted = coValue.unmount();
if (unmounted) {
this.coValues.delete(coValue.id);
}
coValue.unmount();
}
}
}

View File

@@ -1,8 +1,4 @@
import {
CoValueHeader,
Transaction,
VerifiedState,
} from "./coValueCore/verifiedState.js";
import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
import { TRANSACTION_CONFIG } from "./config.js";
import { Signature } from "./crypto/crypto.js";
import { RawCoID, SessionID } from "./ids.js";
@@ -65,6 +61,7 @@ export function exceedsRecommendedSize(
export function knownStateFromContent(content: NewContentMessage) {
const knownState = emptyKnownState(content.id);
knownState.header = Boolean(content.header);
for (const [sessionID, session] of Object.entries(content.new)) {
knownState.sessions[sessionID as SessionID] =

View File

@@ -219,6 +219,8 @@ export class CoValueCore {
this.groupInvalidationSubscription = undefined;
}
this.node.internalDeleteCoValue(this.id);
return true;
}

View File

@@ -0,0 +1,215 @@
import { describe, expect, test } from "vitest";
import { knownStateFromContent } from "../coValueContentMessage.js";
import { emptyKnownState } from "../sync.js";
import type { NewContentMessage } from "../sync.js";
import type { RawCoID, SessionID } from "../ids.js";
import { stableStringify } from "../jsonStringify.js";
import { CO_VALUE_PRIORITY } from "../priority.js";
describe("knownStateFromContent", () => {
const mockCoID: RawCoID = "co_z1234567890abcdef";
const mockSessionID1: SessionID = "sealer_z123/signer_z456_session_z789";
const mockSessionID2: SessionID = "sealer_zabc/signer_zdef_session_zghi";
test("returns empty known state for content with no header and no sessions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: undefined,
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
const expected = emptyKnownState(mockCoID);
expect(result).toEqual(expected);
expect(result.id).toBe(mockCoID);
expect(result.header).toBe(false);
expect(result.sessions).toEqual({});
});
test("sets header to true when content has header", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
uniqueness: null,
createdAt: null,
},
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(true);
expect(result.id).toBe(mockCoID);
expect(result.sessions).toEqual({});
});
test("sets header to false when content has no header", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(false);
expect(result.id).toBe(mockCoID);
expect(result.sessions).toEqual({});
});
test("calculates session states correctly for single session", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
uniqueness: null,
createdAt: null,
},
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 5,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(true);
expect(result.sessions[mockSessionID1]).toBe(8); // 5 + 3
expect(Object.keys(result.sessions)).toHaveLength(1);
});
test("calculates session states correctly for multiple sessions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 3,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
[mockSessionID2]: {
after: 7,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(false);
expect(result.sessions[mockSessionID1]).toBe(5); // 3 + 2
expect(result.sessions[mockSessionID2]).toBe(11); // 7 + 4
expect(Object.keys(result.sessions)).toHaveLength(2);
});
test("handles session with no transactions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 10,
newTransactions: [],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.sessions[mockSessionID1]).toBe(10); // 10 + 0
});
test("handles session with after index 0", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 0,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.sessions[mockSessionID1]).toBe(1); // 0 + 1
});
});

View File

@@ -163,13 +163,10 @@ describe("sync after the garbage collector has run", () => {
"edge -> storage | LOAD Map sessions: empty",
"storage -> edge | CONTENT Group header: true new: After: 0 New: 5",
"storage -> edge | CONTENT Map header: true new: After: 0 New: 1",
"edge -> server | CONTENT Map header: true new: ",
"edge -> client | CONTENT Group header: true new: After: 0 New: 5",
"edge -> client | CONTENT Map header: true new: After: 0 New: 1",
"server -> edge | KNOWN Map sessions: header/1",
"server -> storage | CONTENT Map header: true new: After: 0 New: 1",
"server -> edge | KNOWN Map sessions: header/1",
"server -> storage | CONTENT Map header: true new: ",
"client -> edge | KNOWN Group sessions: header/5",
"client -> edge | KNOWN Map sessions: header/1",
]

View File

@@ -973,7 +973,6 @@ describe("loading coValues from server", () => {
"server -> client | KNOWN Group sessions: header/6",
"server -> client | KNOWN ParentGroup sessions: header/8",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT ParentGroup header: true new: ",
]
`);
});

View File

@@ -164,11 +164,8 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
"core -> storage | CONTENT ParentGroup header: true new: After: 0 New: 6",
"core -> edge-france | KNOWN Group sessions: header/5",
"core -> storage | CONTENT Group header: false new: After: 3 New: 2",
"edge-france -> core | CONTENT ParentGroup header: true new: ",
"edge-france -> storage | CONTENT Map header: true new: After: 0 New: 1",
"edge-france -> core | CONTENT Map header: true new: After: 0 New: 1",
"core -> edge-france | KNOWN ParentGroup sessions: header/6",
"core -> storage | CONTENT ParentGroup header: true new: ",
"core -> edge-france | KNOWN Map sessions: header/1",
"core -> storage | CONTENT Map header: true new: After: 0 New: 1",
"client -> edge-italy | LOAD Map sessions: empty",

View File

@@ -47,8 +47,6 @@ describe("peer reconciliation", () => {
"server -> client | KNOWN Map sessions: empty",
"server -> client | KNOWN Group sessions: header/3",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT Group header: true new: ",
"client -> server | CONTENT Map header: true new: ",
]
`);
});

View File

@@ -86,7 +86,6 @@ describe("client to server upload", () => {
"server -> client | KNOWN ParentGroup sessions: header/6",
"server -> client | KNOWN Group sessions: header/5",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT ParentGroup header: true new: ",
]
`);
});

View File

@@ -1,5 +1,15 @@
# jazz-react
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,16 @@
# jazz-auth-betterauth
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- jazz-betterauth-client-plugin@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,11 @@
# jazz-betterauth-client-plugin
## 0.17.5
### Patch Changes
- jazz-betterauth-server-plugin@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,15 @@
# jazz-betterauth-server-plugin
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# jazz-react-auth-betterauth
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- jazz-auth-betterauth@0.17.5
- jazz-betterauth-client-plugin@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# jazz-run
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- cojson-storage-sqlite@0.17.5
- cojson-transport-ws@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,16 @@
# jazz-tools
## 0.17.5
### Patch Changes
- 5963658: Implement/expose loadUnique and upsertUnique on co.list and co.record
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
- cojson-storage-indexeddb@0.17.5
- cojson-transport-ws@0.17.5
## 0.17.4
### Patch Changes

View File

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

View File

@@ -1,5 +1,5 @@
import type { JsonValue, RawCoList } from "cojson";
import { ControlledAccount, RawAccount } from "cojson";
import type { JsonValue, RawCoList, CoValueUniqueness, RawCoID } from "cojson";
import { ControlledAccount, RawAccount, cojsonInternals } from "cojson";
import { calcPatch } from "fast-myers-diff";
import type {
Account,
@@ -24,6 +24,7 @@ import {
RegisteredSchemas,
SchemaInit,
accessChildByKey,
activeAccountContext,
coField,
coValueClassFromCoValueClassOrSchema,
coValuesCache,
@@ -236,12 +237,21 @@ export class CoList<out Item = any> extends Array<Item> implements CoValue {
static create<L extends CoList>(
this: CoValueClass<L>,
items: L[number][],
options?: { owner: Account | Group } | Account | Group,
options?:
| {
owner: Account | Group;
unique?: CoValueUniqueness["uniqueness"];
}
| Account
| Group,
) {
const { owner } = parseCoValueCreateOptions(options);
const { owner, uniqueness } = parseCoValueCreateOptions(options);
const instance = new this({ init: items, owner });
const raw = owner._raw.createList(
toRawItems(items, instance._schema[ItemsSym], owner),
null,
"private",
uniqueness,
);
Object.defineProperties(instance, {
@@ -546,6 +556,116 @@ export class CoList<out Item = any> extends Array<Item> implements CoValue {
return cl.fromRaw(this._raw) as InstanceType<Cl>;
}
/** @deprecated Use `CoList.upsertUnique` and `CoList.loadUnique` instead. */
static findUnique<L extends CoList>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
) {
return CoList._findUnique(unique, ownerID, as);
}
/** @internal */
static _findUnique<L extends CoList>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
) {
as ||= activeAccountContext.get();
const header = {
type: "colist" as const,
ruleset: {
type: "ownedByGroup" as const,
group: ownerID as RawCoID,
},
meta: null,
uniqueness: unique,
};
const crypto =
as._type === "Anonymous" ? as.node.crypto : as._raw.core.node.crypto;
return cojsonInternals.idforHeader(header, crypto) as ID<L>;
}
/**
* Given some data, updates an existing CoList or initialises a new one if none exists.
*
* Note: This method respects resolve options, and thus can return `null` if the references cannot be resolved.
*
* @example
* ```ts
* const activeItems = await ItemList.upsertUnique(
* {
* value: [item1, item2, item3],
* unique: sourceData.identifier,
* owner: workspace,
* }
* );
* ```
*
* @param options The options for creating or loading the CoList. This includes the intended state of the CoList, its unique identifier, its owner, and the references to resolve.
* @returns Either an existing & modified CoList, or a new initialised CoList if none exists.
* @category Subscription & Loading
*/
static async upsertUnique<
L extends CoList,
const R extends RefsToResolve<L> = true,
>(
this: CoValueClass<L>,
options: {
value: L[number][];
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<L, R>;
},
): Promise<Resolved<L, R> | null> {
let listId = CoList._findUnique(options.unique, options.owner.id);
let list: Resolved<L, R> | null = await loadCoValueWithoutMe(this, listId, {
...options,
loadAs: options.owner._loadedAs,
skipRetry: true,
});
if (!list) {
list = (this as any).create(options.value, {
owner: options.owner,
unique: options.unique,
}) as Resolved<L, R>;
} else {
(list as L).applyDiff(options.value);
}
return await loadCoValueWithoutMe(this, listId, {
...options,
loadAs: options.owner._loadedAs,
skipRetry: true,
});
}
/**
* Loads a CoList by its unique identifier and owner's ID.
* @param unique The unique identifier of the CoList to load.
* @param ownerID The ID of the owner of the CoList.
* @param options Additional options for loading the CoList.
* @returns The loaded CoList, or null if unavailable.
*/
static loadUnique<L extends CoList, const R extends RefsToResolve<L> = true>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<L, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<L, R> | null> {
return loadCoValueWithoutMe(
this,
CoList._findUnique(unique, ownerID, options?.loadAs),
{ ...options, skipRetry: true },
);
}
/**
* Wait for the `CoList` to be uploaded to the other peers.
*

View File

@@ -2,12 +2,14 @@ import {
Account,
CoList,
Group,
ID,
RefsToResolve,
RefsToResolveStrict,
Resolved,
SubscribeListenerOptions,
coOptionalDefiner,
} from "../../../internal.js";
import { CoValueUniqueness } from "cojson";
import { AnonymousJazzAgent } from "../../anonymousJazzAgent.js";
import { CoListInit } from "../typeConverters/CoFieldInit.js";
import { InstanceOrPrimitiveOfSchema } from "../typeConverters/InstanceOrPrimitiveOfSchema.js";
@@ -29,7 +31,13 @@ export class CoListSchema<T extends AnyZodOrCoValueSchema>
create(
items: CoListInit<T>,
options?: { owner: Account | Group } | Account | Group,
options?:
| {
owner: Account | Group;
unique?: CoValueUniqueness["uniqueness"];
}
| Account
| Group,
): CoListInstance<T> {
return this.coValueClass.create(items as any, options) as CoListInstance<T>;
}
@@ -62,6 +70,41 @@ export class CoListSchema<T extends AnyZodOrCoValueSchema>
return this.coValueClass;
}
/** @deprecated Use `CoList.upsertUnique` and `CoList.loadUnique` instead. */
findUnique(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
): ID<CoListInstanceCoValuesNullable<T>> {
return this.coValueClass.findUnique(unique, ownerID, as);
}
upsertUnique<
const R extends RefsToResolve<CoListInstanceCoValuesNullable<T>> = true,
>(options: {
value: CoListInit<T>;
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<CoListInstanceCoValuesNullable<T>, R>;
}): Promise<Resolved<CoListInstanceCoValuesNullable<T>, R> | null> {
// @ts-expect-error
return this.coValueClass.upsertUnique(options);
}
loadUnique<
const R extends RefsToResolve<CoListInstanceCoValuesNullable<T>> = true,
>(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<CoListInstanceCoValuesNullable<T>, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<CoListInstanceCoValuesNullable<T>, R> | null> {
// @ts-expect-error
return this.coValueClass.loadUnique(unique, ownerID, options);
}
optional(): CoOptionalSchema<this> {
return coOptionalDefiner(this);
}

View File

@@ -72,12 +72,37 @@ export interface CoRecordSchema<
) => void,
): () => void;
/** @deprecated Use `CoMap.upsertUnique` and `CoMap.loadUnique` instead. */
findUnique(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
): ID<CoRecordInstanceCoValuesNullable<K, V>>;
upsertUnique<
const R extends RefsToResolve<
CoRecordInstanceCoValuesNullable<K, V>
> = true,
>(options: {
value: Simplify<CoRecordInit<K, V>>;
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<CoRecordInstanceCoValuesNullable<K, V>, R>;
}): Promise<Resolved<CoRecordInstanceCoValuesNullable<K, V>, R> | null>;
loadUnique<
const R extends RefsToResolve<
CoRecordInstanceCoValuesNullable<K, V>
> = true,
>(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<CoRecordInstanceCoValuesNullable<K, V>, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<CoRecordInstanceCoValuesNullable<K, V>, R> | null>;
getCoValueClass: () => typeof CoMap;
optional(): CoOptionalSchema<this>;

View File

@@ -863,6 +863,173 @@ describe("CoList subscription", async () => {
});
});
describe("CoList unique methods", () => {
test("loadUnique returns existing list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const originalList = ItemList.create(["item1", "item2", "item3"], {
owner: group,
unique: "test-list",
});
const foundList = await ItemList.loadUnique("test-list", group.id);
expect(foundList).toEqual(originalList);
expect(foundList?.length).toBe(3);
expect(foundList?.[0]).toBe("item1");
});
test("loadUnique returns null for non-existent list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const foundList = await ItemList.loadUnique("non-existent", group.id);
expect(foundList).toBeNull();
});
test("upsertUnique creates new list when none exists", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const sourceData = ["item1", "item2", "item3"];
const result = await ItemList.upsertUnique({
value: sourceData,
unique: "new-list",
owner: group,
});
expect(result).not.toBeNull();
expect(result?.length).toBe(3);
expect(result?.[0]).toBe("item1");
expect(result?.[1]).toBe("item2");
expect(result?.[2]).toBe("item3");
});
test("upsertUnique updates existing list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
// Create initial list
const originalList = ItemList.create(["original1", "original2"], {
owner: group,
unique: "update-list",
});
// Upsert with new data
const updatedList = await ItemList.upsertUnique({
value: ["updated1", "updated2", "updated3"],
unique: "update-list",
owner: group,
});
expect(updatedList).toEqual(originalList); // Should be the same instance
expect(updatedList?.length).toBe(3);
expect(updatedList?.[0]).toBe("updated1");
expect(updatedList?.[1]).toBe("updated2");
expect(updatedList?.[2]).toBe("updated3");
});
test("upsertUnique with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemList = co.list(Item);
const group = Group.create();
const items = [
Item.create({ name: "First", value: 1 }, group),
Item.create({ name: "Second", value: 2 }, group),
];
const result = await ItemList.upsertUnique({
value: items,
unique: "item-list",
owner: group,
resolve: { $each: true },
});
expect(result).not.toBeNull();
expect(result?.length).toBe(2);
expect(result?.[0]?.name).toBe("First");
expect(result?.[1]?.name).toBe("Second");
});
test("upsertUnique updates list with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemList = co.list(Item);
const group = Group.create();
// Create initial list
const initialItems = [Item.create({ name: "Initial", value: 0 }, group)];
const originalList = ItemList.create(initialItems, {
owner: group,
unique: "updateable-item-list",
});
// Upsert with new items
const newItems = [
Item.create({ name: "Updated", value: 1 }, group),
Item.create({ name: "Added", value: 2 }, group),
];
const updatedList = await ItemList.upsertUnique({
value: newItems,
unique: "updateable-item-list",
owner: group,
resolve: { $each: true },
});
expect(updatedList).toEqual(originalList); // Should be the same instance
expect(updatedList?.length).toBe(2);
expect(updatedList?.[0]?.name).toBe("Updated");
expect(updatedList?.[1]?.name).toBe("Added");
});
test("findUnique returns correct ID", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const originalList = ItemList.create(["test"], {
owner: group,
unique: "find-test",
});
const foundId = ItemList.findUnique("find-test", group.id);
expect(foundId).toBe(originalList.id);
});
test("upsertUnique with resolve options", async () => {
const Category = co.map({ title: z.string() });
const Item = co.map({
name: z.string(),
category: Category,
});
const ItemList = co.list(Item);
const group = Group.create();
const category = Category.create({ title: "Category 1" }, group);
const items = [Item.create({ name: "Item 1", category }, group)];
const result = await ItemList.upsertUnique({
value: items,
unique: "resolved-list",
owner: group,
resolve: { $each: { category: true } },
});
expect(result).not.toBeNull();
expect(result?.length).toBe(1);
expect(result?.[0]?.name).toBe("Item 1");
expect(result?.[0]?.category?.title).toBe("Category 1");
});
});
describe("co.list schema", () => {
test("can access the inner schema of a co.list", () => {
const Keywords = co.list(co.plainText());

View File

@@ -460,3 +460,107 @@ describe("CoMap.Record", async () => {
}
});
});
describe("CoRecord unique methods", () => {
test("loadUnique returns existing record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const originalRecord = ItemRecord.create(
{ item1: 1, item2: 2, item3: 3 },
{ owner: group, unique: "test-record" },
);
const foundRecord = await ItemRecord.loadUnique("test-record", group.id);
expect(foundRecord).toEqual(originalRecord);
expect(foundRecord?.item1).toBe(1);
expect(foundRecord?.item2).toBe(2);
});
test("loadUnique returns null for non-existent record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const foundRecord = await ItemRecord.loadUnique("non-existent", group.id);
expect(foundRecord).toBeNull();
});
test("upsertUnique creates new record when none exists", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const sourceData = { item1: 1, item2: 2, item3: 3 };
const result = await ItemRecord.upsertUnique({
value: sourceData,
unique: "new-record",
owner: group,
});
expect(result).not.toBeNull();
expect(result?.item1).toBe(1);
expect(result?.item2).toBe(2);
expect(result?.item3).toBe(3);
});
test("upsertUnique updates existing record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
// Create initial record
const originalRecord = ItemRecord.create(
{ original1: 1, original2: 2 },
{ owner: group, unique: "update-record" },
);
// Upsert with new data
const updatedRecord = await ItemRecord.upsertUnique({
value: { updated1: 10, updated2: 20, updated3: 30 },
unique: "update-record",
owner: group,
});
expect(updatedRecord).toEqual(originalRecord); // Should be the same instance
expect(updatedRecord?.updated1).toBe(10);
expect(updatedRecord?.updated2).toBe(20);
expect(updatedRecord?.updated3).toBe(30);
});
test("upsertUnique with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemRecord = co.record(z.string(), Item);
const group = Group.create();
const items = {
first: Item.create({ name: "First", value: 1 }, group),
second: Item.create({ name: "Second", value: 2 }, group),
};
const result = await ItemRecord.upsertUnique({
value: items,
unique: "item-record",
owner: group,
resolve: { first: true, second: true },
});
expect(result).not.toBeNull();
expect(result?.first?.name).toBe("First");
expect(result?.second?.name).toBe("Second");
});
test("findUnique returns correct ID", async () => {
const ItemRecord = co.record(z.string(), z.string());
const group = Group.create();
const originalRecord = ItemRecord.create(
{ test: "value" },
{ owner: group, unique: "find-test" },
);
const foundId = ItemRecord.findUnique("find-test", group.id);
expect(foundId).toBe(originalRecord.id);
});
});

8
pnpm-lock.yaml generated
View File

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

View File

@@ -1,5 +1,12 @@
# jazz-react-tailwind-starter
## 0.0.149
### Patch Changes
- Updated dependencies [5963658]
- jazz-tools@0.17.5
## 0.0.148
### Patch Changes

View File

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

View File

@@ -1,5 +1,12 @@
# svelte-passkey-auth
## 0.0.123
### Patch Changes
- Updated dependencies [5963658]
- jazz-tools@0.17.5
## 0.0.122
### Patch Changes

View File

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