jazz-tools 0.18.18 → 0.18.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +45 -45
  2. package/CHANGELOG.md +27 -0
  3. package/dist/better-auth/auth/client.d.ts.map +1 -1
  4. package/dist/better-auth/auth/client.js +3 -2
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/browser/index.js +2 -2
  7. package/dist/browser/index.js.map +1 -1
  8. package/dist/{chunk-FHRKDKDY.js → chunk-TVHI2UMO.js} +49 -2
  9. package/dist/chunk-TVHI2UMO.js.map +1 -0
  10. package/dist/index.js +1 -1
  11. package/dist/react-core/hooks.d.ts.map +1 -1
  12. package/dist/react-core/index.js +2 -2
  13. package/dist/react-core/index.js.map +1 -1
  14. package/dist/testing.js +1 -1
  15. package/dist/tools/coValues/coList.d.ts.map +1 -1
  16. package/dist/tools/coValues/coMap.d.ts +2 -2
  17. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  18. package/dist/tools/coValues/coPlainText.d.ts.map +1 -1
  19. package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts +1 -1
  20. package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
  21. package/dist/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.d.ts +1 -1
  22. package/dist/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.d.ts.map +1 -1
  23. package/dist/tools/implementation/zodSchema/zodReExport.d.ts +1 -1
  24. package/dist/tools/implementation/zodSchema/zodReExport.d.ts.map +1 -1
  25. package/package.json +5 -5
  26. package/src/better-auth/auth/client.ts +3 -2
  27. package/src/better-auth/auth/tests/client.test.ts +22 -0
  28. package/src/browser/auth/PasskeyAuth.ts +2 -2
  29. package/src/react-core/hooks.ts +3 -2
  30. package/src/react-core/tests/useInboxSender.test.ts +37 -3
  31. package/src/tools/coValues/coList.ts +11 -0
  32. package/src/tools/coValues/coMap.ts +5 -5
  33. package/src/tools/coValues/coPlainText.ts +9 -0
  34. package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +47 -2
  35. package/src/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.ts +9 -4
  36. package/src/tools/implementation/zodSchema/zodReExport.ts +1 -0
  37. package/src/tools/subscribe/index.ts +1 -1
  38. package/src/tools/tests/coList.test.ts +34 -0
  39. package/src/tools/tests/coMap.test.ts +26 -2
  40. package/src/tools/tests/coPlainText.test.ts +25 -1
  41. package/src/tools/tests/subscribe.test.ts +41 -0
  42. package/src/tools/tests/zod.test.ts +131 -0
  43. package/dist/chunk-FHRKDKDY.js.map +0 -1
  44. package/jazz-tools-0.18.6.tgz +0 -0
@@ -739,9 +739,20 @@ export class CoListJazzApi<L extends CoList> extends CoValueJazzApi<L> {
739
739
  : undefined;
740
740
 
741
741
  const patches = [...calcPatch(current, result, comparator)];
742
+
743
+ if (patches.length === 0) {
744
+ return this.coList;
745
+ }
746
+
747
+ // Turns off updates in the middle of applyDiff to improve the performance
748
+ this.raw.core.pauseNotifyUpdate();
749
+
742
750
  for (const [from, to, insert] of patches.reverse()) {
743
751
  this.splice(from, to - from, ...insert);
744
752
  }
753
+
754
+ this.raw.core.resumeNotifyUpdate();
755
+
745
756
  return this.coList;
746
757
  }
747
758
 
@@ -34,6 +34,7 @@ import {
34
34
  CoValueBase,
35
35
  CoValueJazzApi,
36
36
  ItemsSym,
37
+ NotNull,
37
38
  Ref,
38
39
  RegisteredSchemas,
39
40
  SchemaInit,
@@ -760,11 +761,10 @@ class CoMapJazzApi<M extends CoMap> extends CoValueJazzApi<M> {
760
761
  ? Key
761
762
  : never]?: RefIfCoValue<M[Key]>;
762
763
  } & {
763
- [Key in CoKeys<M> as M[Key] extends undefined
764
- ? never
765
- : M[Key] extends CoValue
766
- ? Key
767
- : never]: RefIfCoValue<M[Key]>;
764
+ // Non-loaded CoValue refs (i.e. refs with type CoValue | null) are still required refs
765
+ [Key in CoKeys<M> as NotNull<M[Key]> extends CoValue
766
+ ? Key
767
+ : never]: RefIfCoValue<M[Key]>;
768
768
  }
769
769
  > {
770
770
  return makeRefs<CoKeys<this>>(
@@ -246,6 +246,13 @@ export class CoTextJazzApi<T extends CoPlainText> extends CoValueJazzApi<T> {
246
246
  // Calculate the diff on grapheme arrays
247
247
  const patches = [...calcPatch(currentGraphemes, otherGraphemes)];
248
248
 
249
+ if (patches.length === 0) {
250
+ return;
251
+ }
252
+
253
+ // Turns off updates in the middle of applyDiff to improve the performance
254
+ this.raw.core.pauseNotifyUpdate();
255
+
249
256
  // Apply patches in reverse order to avoid index shifting issues
250
257
  for (const [from, to, insert] of patches.reverse()) {
251
258
  if (to > from) {
@@ -256,6 +263,8 @@ export class CoTextJazzApi<T extends CoPlainText> extends CoValueJazzApi<T> {
256
263
  this.coText.insertBefore(from, this.raw.fromGraphemes(insert));
257
264
  }
258
265
  }
266
+
267
+ this.raw.core.resumeNotifyUpdate();
259
268
  }
260
269
 
261
270
  /**
@@ -1,3 +1,4 @@
1
+ import type { JsonValue } from "cojson";
1
2
  import { CoValueClass, isCoValueClass } from "../../../internal.js";
2
3
  import { coField } from "../../schema.js";
3
4
  import { CoreCoValueSchema } from "../schemaTypes/CoValueSchema.js";
@@ -32,11 +33,31 @@ export type SchemaField =
32
33
  | z.core.$ZodLazy<z.core.$ZodType>
33
34
  | z.core.$ZodTemplateLiteral<any>
34
35
  | z.core.$ZodLiteral<any>
35
- | z.core.$ZodCatch<z.core.$ZodType>
36
36
  | z.core.$ZodEnum<any>
37
+ | z.core.$ZodCodec<z.core.$ZodType, z.core.$ZodType>
37
38
  | z.core.$ZodDefault<z.core.$ZodType>
38
39
  | z.core.$ZodCatch<z.core.$ZodType>;
39
40
 
41
+ function makeCodecCoField(
42
+ codec: z.core.$ZodCodec<z.core.$ZodType, z.core.$ZodType>,
43
+ ) {
44
+ return coField.optional.encoded({
45
+ encode: (value: any) => {
46
+ if (value === undefined) return undefined as unknown as JsonValue;
47
+ if (value === null) return null;
48
+ return codec._zod.def.reverseTransform(value, {
49
+ value,
50
+ issues: [],
51
+ }) as JsonValue;
52
+ },
53
+ decode: (value) => {
54
+ if (value === null) return null;
55
+ if (value === undefined) return undefined;
56
+ return codec._zod.def.transform(value, { value, issues: [] });
57
+ },
58
+ });
59
+ }
60
+
40
61
  export function schemaFieldToCoFieldDef(schema: SchemaField) {
41
62
  if (isCoValueClass(schema)) {
42
63
  return coField.ref(schema);
@@ -54,7 +75,7 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
54
75
  zodSchemaDef.type === "optional" ||
55
76
  zodSchemaDef.type === "nullable"
56
77
  ) {
57
- const inner = zodSchemaDef.innerType as ZodPrimitiveSchema;
78
+ const inner = zodSchemaDef.innerType as SchemaField;
58
79
  const coFieldDef: any = schemaFieldToCoFieldDef(inner);
59
80
  if (
60
81
  zodSchemaDef.type === "nullable" &&
@@ -137,6 +158,30 @@ export function schemaFieldToCoFieldDef(schema: SchemaField) {
137
158
  "z.union()/z.discriminatedUnion() of collaborative types is not supported. Use co.discriminatedUnion() instead.",
138
159
  );
139
160
  }
161
+ } else if (zodSchemaDef.type === "pipe") {
162
+ const isCodec =
163
+ zodSchemaDef.transform !== undefined &&
164
+ zodSchemaDef.reverseTransform !== undefined;
165
+
166
+ if (!isCodec) {
167
+ throw new Error(
168
+ "z.pipe() is not supported. Only z.codec() is supported.",
169
+ );
170
+ }
171
+
172
+ try {
173
+ schemaFieldToCoFieldDef(zodSchemaDef.in as SchemaField);
174
+ } catch (error) {
175
+ if (error instanceof Error) {
176
+ error.message = `z.codec() is only supported if the input schema is already supported. ${error.message}`;
177
+ }
178
+
179
+ throw error;
180
+ }
181
+
182
+ return makeCodecCoField(
183
+ schema as z.core.$ZodCodec<z.core.$ZodType, z.core.$ZodType>,
184
+ );
140
185
  } else {
141
186
  throw new Error(
142
187
  `Unsupported zod type: ${(schema._zod?.def as any)?.type || JSON.stringify(schema)}`,
@@ -71,8 +71,13 @@ export type TypeOfZodSchema<S extends z.core.$ZodType> =
71
71
  infer Default extends z.core.$ZodType
72
72
  >
73
73
  ? TypeOfZodSchema<Default>
74
- : S extends z.core.$ZodCatch<
75
- infer Catch extends z.core.$ZodType
74
+ : S extends z.core.$ZodCodec<
75
+ any,
76
+ infer Out extends z.core.$ZodType
76
77
  >
77
- ? TypeOfZodSchema<Catch>
78
- : never;
78
+ ? Out["_zod"]["output"]
79
+ : S extends z.core.$ZodCatch<
80
+ infer Catch extends z.core.$ZodType
81
+ >
82
+ ? TypeOfZodSchema<Catch>
83
+ : never;
@@ -35,6 +35,7 @@ export {
35
35
  // record,
36
36
  // intersection,
37
37
  int,
38
+ codec,
38
39
  optional,
39
40
  array,
40
41
  tuple,
@@ -18,7 +18,7 @@ export function getSubscriptionScope<D extends CoValue>(value: D) {
18
18
  });
19
19
 
20
20
  Object.defineProperty(value.$jazz, "_subscriptionScope", {
21
- value: subscriptionScope,
21
+ value: newSubscriptionScope,
22
22
  writable: false,
23
23
  enumerable: false,
24
24
  configurable: false,
@@ -680,6 +680,40 @@ describe("CoList applyDiff operations", async () => {
680
680
  list.$jazz.applyDiff(["e", "c", "new", "y", "x"]);
681
681
  expect(list.$jazz.raw.asArray()).toEqual(["e", "c", "new", "y", "x"]);
682
682
  });
683
+
684
+ test("applyDiff should emit a single update", () => {
685
+ const TestMap = co.map({
686
+ type: z.string(),
687
+ });
688
+
689
+ const TestList = co.list(TestMap);
690
+
691
+ const bread = TestMap.create({ type: "bread" }, me);
692
+ const butter = TestMap.create({ type: "butter" }, me);
693
+ const onion = TestMap.create({ type: "onion" }, me);
694
+
695
+ const list = TestList.create([bread, butter, onion], me);
696
+
697
+ const updateFn = vi.fn();
698
+
699
+ const unsubscribe = TestList.subscribe(
700
+ list.$jazz.id,
701
+ {
702
+ resolve: {
703
+ $each: true,
704
+ },
705
+ },
706
+ updateFn,
707
+ );
708
+
709
+ updateFn.mockClear();
710
+
711
+ list.$jazz.applyDiff([bread]);
712
+
713
+ expect(updateFn).toHaveBeenCalledTimes(1);
714
+
715
+ unsubscribe();
716
+ });
683
717
  });
684
718
 
685
719
  describe("CoList resolution", async () => {
@@ -1150,6 +1150,30 @@ describe("CoMap resolution", async () => {
1150
1150
  });
1151
1151
  });
1152
1152
 
1153
+ test("obtaining coMap refs", async () => {
1154
+ const Dog = co.map({
1155
+ name: z.string().optional(),
1156
+ breed: z.string(),
1157
+ owner: co.plainText(),
1158
+ get parent() {
1159
+ return co.optional(Dog);
1160
+ },
1161
+ });
1162
+
1163
+ const dog = Dog.create({
1164
+ name: "Rex",
1165
+ breed: "Labrador",
1166
+ owner: "John",
1167
+ parent: { name: "Fido", breed: "Labrador", owner: "Jane" },
1168
+ });
1169
+
1170
+ const refs = dog.$jazz.refs;
1171
+
1172
+ expect(Object.keys(refs)).toEqual(["owner", "parent"]);
1173
+ expect(refs.owner.id).toEqual(dog.owner.$jazz.id);
1174
+ expect(refs.parent?.id).toEqual(dog.parent!.$jazz.id);
1175
+ });
1176
+
1153
1177
  test("accessing the value refs", async () => {
1154
1178
  const Dog = co.map({
1155
1179
  name: z.string(),
@@ -1181,9 +1205,9 @@ describe("CoMap resolution", async () => {
1181
1205
 
1182
1206
  assert(loadedPerson);
1183
1207
 
1184
- expect(loadedPerson.$jazz.refs.dog?.id).toBe(person.dog!.$jazz.id);
1208
+ expect(loadedPerson.$jazz.refs.dog.id).toBe(person.dog.$jazz.id);
1185
1209
 
1186
- const dog = await loadedPerson.$jazz.refs.dog?.load();
1210
+ const dog = await loadedPerson.$jazz.refs.dog.load();
1187
1211
 
1188
1212
  assert(dog);
1189
1213
 
@@ -1,6 +1,6 @@
1
1
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
2
2
  import { Channel } from "queueueue";
3
- import { describe, expect, test } from "vitest";
3
+ import { describe, expect, test, vi } from "vitest";
4
4
  import {
5
5
  Account,
6
6
  cojsonInternals,
@@ -94,6 +94,30 @@ describe("CoPlainText", () => {
94
94
  text.$jazz.applyDiff(`😊👋 안녕!`);
95
95
  expect(text.toString()).toEqual(`😊👋 안녕!`);
96
96
  });
97
+
98
+ test("applyDiff should emit a single update", () => {
99
+ const Text = co.plainText();
100
+
101
+ const text = Text.create(`😊`, { owner: me });
102
+
103
+ const updateFn = vi.fn();
104
+
105
+ const unsubscribe = Text.subscribe(
106
+ text.$jazz.id,
107
+ {
108
+ loadAs: me,
109
+ },
110
+ updateFn,
111
+ );
112
+
113
+ updateFn.mockClear();
114
+
115
+ text.$jazz.applyDiff(`😊👋 안녕!`);
116
+
117
+ expect(updateFn).toHaveBeenCalledTimes(1);
118
+
119
+ unsubscribe();
120
+ });
97
121
  });
98
122
 
99
123
  describe("Properties", () => {
@@ -20,6 +20,7 @@ import {
20
20
  setupJazzTestSync,
21
21
  } from "../testing.js";
22
22
  import { setupAccount, waitFor } from "./utils.js";
23
+ import { getSubscriptionScope } from "../subscribe/index.js";
23
24
 
24
25
  cojsonInternals.setCoValueLoadingRetryDelay(300);
25
26
 
@@ -1278,3 +1279,43 @@ describe("subscribeToCoValue", () => {
1278
1279
  expect(result.data[chunks]).toBe("new entry");
1279
1280
  });
1280
1281
  });
1282
+
1283
+ describe("getSubscriptionScope", () => {
1284
+ const Person = co.map({
1285
+ name: z.string(),
1286
+ });
1287
+ let person: co.output<typeof Person>;
1288
+
1289
+ beforeEach(async () => {
1290
+ await createJazzTestAccount({
1291
+ isCurrentActiveAccount: true,
1292
+ creationProps: { name: "Hermes Puggington" },
1293
+ });
1294
+
1295
+ person = Person.create({ name: "John" });
1296
+ });
1297
+
1298
+ describe("when the coValue doesn't have a subscription scope", () => {
1299
+ it("creates a new subscription scope", () => {
1300
+ expect(person.$jazz._subscriptionScope).toBeUndefined();
1301
+ const subscriptionScope = getSubscriptionScope(person);
1302
+ expect(subscriptionScope).toBeDefined();
1303
+ });
1304
+
1305
+ it("updates the subscription scope in the coValue", () => {
1306
+ const subscriptionScope = getSubscriptionScope(person);
1307
+ expect(person.$jazz._subscriptionScope).toBeDefined();
1308
+ expect(person.$jazz._subscriptionScope).toBe(subscriptionScope);
1309
+ });
1310
+ });
1311
+
1312
+ describe("when the coValue already has a subscription scope", () => {
1313
+ it("returns that subscription scope", async () => {
1314
+ const loadedPerson = await Person.load(person.$jazz.id);
1315
+ assert(loadedPerson);
1316
+ const subscriptionScope = loadedPerson.$jazz._subscriptionScope;
1317
+ expect(subscriptionScope).toBeDefined();
1318
+ expect(getSubscriptionScope(loadedPerson)).toBe(subscriptionScope);
1319
+ });
1320
+ });
1321
+ });
@@ -511,6 +511,137 @@ describe("co.map and Zod schema compatibility", () => {
511
511
  expect(map.readonly).toEqual({ name: "John" });
512
512
  });
513
513
  });
514
+
515
+ describe("Codec types", () => {
516
+ class DateRange {
517
+ constructor(
518
+ public start: Date,
519
+ public end: Date,
520
+ ) {}
521
+
522
+ isDateInRange(date: Date) {
523
+ return date >= this.start && date <= this.end;
524
+ }
525
+ }
526
+
527
+ const dateRangeCodec = z.codec(
528
+ z.tuple([z.string(), z.string()]),
529
+ z.z.instanceof(DateRange),
530
+ {
531
+ encode: (value) =>
532
+ [value.start.toISOString(), value.end.toISOString()] as [
533
+ string,
534
+ string,
535
+ ],
536
+ decode: ([start, end]) => {
537
+ return new DateRange(new Date(start), new Date(end));
538
+ },
539
+ },
540
+ );
541
+
542
+ it("should handle codec field", async () => {
543
+ const schema = co.map({
544
+ range: dateRangeCodec,
545
+ });
546
+
547
+ const map = schema.create({
548
+ range: new DateRange(new Date("2025-01-01"), new Date("2025-01-31")),
549
+ });
550
+
551
+ expect(map.range.isDateInRange(new Date("2025-01-15"))).toEqual(true);
552
+ });
553
+
554
+ it("should handle codec field with RegExp", async () => {
555
+ const schema = co.map({
556
+ regexp: z.codec(z.string(), z.z.instanceof(RegExp), {
557
+ encode: (value) => value.toString(),
558
+ decode: (value) => {
559
+ const [, pattern, flags] = value.match(/^\/(.*)\/([a-z]*)$/i)!;
560
+ if (!pattern) throw new Error("Invalid RegExp string");
561
+ return new RegExp(pattern, flags);
562
+ },
563
+ }),
564
+ });
565
+
566
+ const map = schema.create({
567
+ regexp: /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
568
+ });
569
+
570
+ expect(map.regexp.test("2001-01-31")).toEqual(true);
571
+ });
572
+
573
+ it("should handle optional codec field", async () => {
574
+ const schema = co.map({
575
+ range: dateRangeCodec.optional(),
576
+ });
577
+ const map = schema.create({});
578
+
579
+ expect(map.range).toBeUndefined();
580
+ expect(map.$jazz.has("range")).toEqual(false);
581
+ });
582
+
583
+ it("should handle nullable codec field", async () => {
584
+ const schema = co.map({
585
+ range: dateRangeCodec.nullable(),
586
+ });
587
+ const map = schema.create({ range: null });
588
+
589
+ expect(map.range).toBeNull();
590
+
591
+ map.$jazz.set(
592
+ "range",
593
+ new DateRange(new Date("2025-01-01"), new Date("2025-01-31")),
594
+ );
595
+ expect(map.range?.isDateInRange(new Date("2025-01-15"))).toEqual(true);
596
+ });
597
+
598
+ it("should handle nullish codec field", async () => {
599
+ const schema = co.map({
600
+ range: dateRangeCodec.nullish(),
601
+ });
602
+
603
+ const map = schema.create({});
604
+ expect(map.range).toBeUndefined();
605
+ expect(map.$jazz.has("range")).toEqual(false);
606
+
607
+ map.$jazz.set("range", undefined);
608
+ expect(map.range).toBeUndefined();
609
+ expect(map.$jazz.has("range")).toEqual(true);
610
+
611
+ map.$jazz.set("range", null);
612
+ expect(map.range).toBeNull();
613
+
614
+ map.$jazz.set(
615
+ "range",
616
+ new DateRange(new Date("2025-01-01"), new Date("2025-01-31")),
617
+ );
618
+ expect(map.range?.isDateInRange(new Date("2025-01-15"))).toEqual(true);
619
+ });
620
+
621
+ it("should not handle codec field with unsupported inner field", async () => {
622
+ const schema = co.map({
623
+ record: z.codec(
624
+ z.z.map(z.string(), z.string()),
625
+ z.z.record(z.string(), z.string()),
626
+ {
627
+ encode: (value) => new Map(Object.entries(value)),
628
+ decode: (value) => Object.fromEntries(value.entries()),
629
+ },
630
+ ),
631
+ });
632
+
633
+ expect(() =>
634
+ schema.create({
635
+ record: {
636
+ key1: "value1",
637
+ key2: "value2",
638
+ },
639
+ }),
640
+ ).toThrow(
641
+ "z.codec() is only supported if the input schema is already supported. Unsupported zod type: map",
642
+ );
643
+ });
644
+ });
514
645
  });
515
646
 
516
647
  describe("z.object() and CoValue schema compatibility", () => {