jazz-tools 0.19.2 → 0.19.4

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 (89) hide show
  1. package/.svelte-kit/__package__/jazz.class.svelte.d.ts +2 -2
  2. package/.svelte-kit/__package__/jazz.class.svelte.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/jazz.class.svelte.js +15 -17
  4. package/.turbo/turbo-build.log +54 -54
  5. package/CHANGELOG.md +24 -0
  6. package/dist/{chunk-NCNM6UDZ.js → chunk-PT7FCV26.js} +148 -78
  7. package/dist/chunk-PT7FCV26.js.map +1 -0
  8. package/dist/index.js +14 -7
  9. package/dist/index.js.map +1 -1
  10. package/dist/inspector/{custom-element-ABVPHX53.js → custom-element-P76EIWEV.js} +322 -146
  11. package/dist/inspector/custom-element-P76EIWEV.js.map +1 -0
  12. package/dist/inspector/index.js +302 -126
  13. package/dist/inspector/index.js.map +1 -1
  14. package/dist/inspector/register-custom-element.js +1 -1
  15. package/dist/inspector/tests/viewer/co-plain-text-view.test.d.ts +2 -0
  16. package/dist/inspector/tests/viewer/co-plain-text-view.test.d.ts.map +1 -0
  17. package/dist/inspector/utils/history.d.ts +5 -1
  18. package/dist/inspector/utils/history.d.ts.map +1 -1
  19. package/dist/inspector/utils/permissions.d.ts +3 -0
  20. package/dist/inspector/utils/permissions.d.ts.map +1 -0
  21. package/dist/inspector/viewer/co-map-view.d.ts.map +1 -1
  22. package/dist/inspector/viewer/co-plain-text-view.d.ts +4 -2
  23. package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -1
  24. package/dist/inspector/viewer/grid-view.d.ts.map +1 -1
  25. package/dist/inspector/viewer/page.d.ts.map +1 -1
  26. package/dist/inspector/viewer/use-resolve-covalue.d.ts +0 -1
  27. package/dist/inspector/viewer/use-resolve-covalue.d.ts.map +1 -1
  28. package/dist/react-core/hooks.d.ts.map +1 -1
  29. package/dist/react-core/index.js +4 -17
  30. package/dist/react-core/index.js.map +1 -1
  31. package/dist/svelte/jazz.class.svelte.d.ts +2 -2
  32. package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
  33. package/dist/svelte/jazz.class.svelte.js +15 -17
  34. package/dist/testing.js +1 -1
  35. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  36. package/dist/tools/coValues/group.d.ts.map +1 -1
  37. package/dist/tools/coValues/interfaces.d.ts +7 -6
  38. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  39. package/dist/tools/coValues/promise.d.ts +9 -0
  40. package/dist/tools/coValues/promise.d.ts.map +1 -0
  41. package/dist/tools/coValues/request.d.ts.map +1 -1
  42. package/dist/tools/exports.d.ts +1 -1
  43. package/dist/tools/exports.d.ts.map +1 -1
  44. package/dist/tools/implementation/refs.d.ts +1 -1
  45. package/dist/tools/implementation/refs.d.ts.map +1 -1
  46. package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts +3 -1
  47. package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
  48. package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
  49. package/dist/tools/subscribe/SubscriptionScope.d.ts +5 -2
  50. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  51. package/dist/tools/subscribe/index.d.ts +1 -1
  52. package/dist/tools/subscribe/index.d.ts.map +1 -1
  53. package/dist/tools/subscribe/types.d.ts +2 -1
  54. package/dist/tools/subscribe/types.d.ts.map +1 -1
  55. package/dist/tools/tests/SubscriptionScope.test.d.ts +2 -0
  56. package/dist/tools/tests/SubscriptionScope.test.d.ts.map +1 -0
  57. package/package.json +4 -4
  58. package/src/inspector/tests/utils/history.test.ts +233 -2
  59. package/src/inspector/tests/viewer/co-plain-text-view.test.tsx +125 -0
  60. package/src/inspector/tests/viewer/comap-view.test.tsx +309 -1
  61. package/src/inspector/tests/viewer/history-view.test.tsx +134 -2
  62. package/src/inspector/utils/history.ts +168 -1
  63. package/src/inspector/utils/permissions.ts +10 -0
  64. package/src/inspector/viewer/co-map-view.tsx +27 -15
  65. package/src/inspector/viewer/co-plain-text-view.tsx +102 -3
  66. package/src/inspector/viewer/grid-view.tsx +2 -1
  67. package/src/inspector/viewer/history-view.tsx +5 -23
  68. package/src/inspector/viewer/page.tsx +8 -1
  69. package/src/inspector/viewer/use-resolve-covalue.ts +2 -6
  70. package/src/react-core/hooks.ts +5 -29
  71. package/src/svelte/jazz.class.svelte.ts +16 -34
  72. package/src/tools/coValues/coFeed.ts +10 -7
  73. package/src/tools/coValues/coMap.ts +10 -7
  74. package/src/tools/coValues/group.ts +6 -2
  75. package/src/tools/coValues/interfaces.ts +48 -28
  76. package/src/tools/coValues/promise.ts +34 -0
  77. package/src/tools/coValues/request.ts +12 -8
  78. package/src/tools/exports.ts +1 -0
  79. package/src/tools/implementation/refs.ts +9 -17
  80. package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +62 -30
  81. package/src/tools/implementation/zodSchema/unionUtils.ts +3 -4
  82. package/src/tools/subscribe/SubscriptionScope.ts +45 -2
  83. package/src/tools/subscribe/index.ts +28 -13
  84. package/src/tools/subscribe/types.ts +5 -2
  85. package/src/tools/tests/SubscriptionScope.test.ts +397 -0
  86. package/src/tools/tests/deepLoading.test.ts +22 -0
  87. package/src/tools/tests/subscribe.test.ts +69 -0
  88. package/dist/chunk-NCNM6UDZ.js.map +0 -1
  89. package/dist/inspector/custom-element-ABVPHX53.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  // @vitest-environment happy-dom
2
- import { afterEach, beforeAll, describe, expect, it } from "vitest";
2
+ import { afterEach, assert, beforeAll, describe, expect, it } from "vitest";
3
3
  import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
4
4
  import { co, z } from "jazz-tools";
5
5
  import {
@@ -578,4 +578,312 @@ describe("CoMapView", async () => {
578
578
  });
579
579
  });
580
580
  });
581
+
582
+ describe("Permissions", () => {
583
+ it("should disable Add Property button for reader account", async () => {
584
+ const reader = await createJazzTestAccount();
585
+ const group = co.group().create({ owner: account });
586
+ group.addMember(reader, "reader");
587
+
588
+ const schema = co.map({
589
+ pet: z.string(),
590
+ });
591
+
592
+ const value = schema.create({ pet: "dog" }, group);
593
+
594
+ const valueOnReader = await schema.load(value.$jazz.id, {
595
+ loadAs: reader,
596
+ });
597
+ assert(valueOnReader.$isLoaded);
598
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
599
+
600
+ render(
601
+ <CoMapView
602
+ coValue={valueOnReader.$jazz.raw}
603
+ data={data}
604
+ node={reader.$jazz.localNode}
605
+ onNavigate={() => {}}
606
+ />,
607
+ );
608
+
609
+ const addButton = screen.getByTitle("Add Property");
610
+ expect(addButton).toBeDefined();
611
+ expect((addButton as HTMLButtonElement).disabled).toBe(true);
612
+ });
613
+
614
+ it("should enable Add Property button for writer account", async () => {
615
+ const writer = await createJazzTestAccount();
616
+ const group = co.group().create({ owner: account });
617
+ group.addMember(writer, "writer");
618
+
619
+ const schema = co.map({
620
+ pet: z.string(),
621
+ });
622
+
623
+ const value = schema.create({ pet: "dog" }, group);
624
+
625
+ const valueOnWriter = await schema.load(value.$jazz.id, {
626
+ loadAs: writer,
627
+ });
628
+ assert(valueOnWriter.$isLoaded);
629
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
630
+
631
+ render(
632
+ <CoMapView
633
+ coValue={valueOnWriter.$jazz.raw}
634
+ data={data}
635
+ node={writer.$jazz.localNode}
636
+ onNavigate={() => {}}
637
+ />,
638
+ );
639
+
640
+ const addButton = screen.getByTitle("Add Property");
641
+ expect(addButton).toBeDefined();
642
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
643
+ });
644
+
645
+ it("should hide restore buttons for reader account when multiple timestamps exist", async () => {
646
+ const reader = await createJazzTestAccount();
647
+ const group = co.group().create({ owner: account });
648
+ group.addMember(reader, "reader");
649
+
650
+ const schema = co.map({
651
+ pet: z.string(),
652
+ });
653
+
654
+ const value = schema.create({ pet: "dog" }, group);
655
+ await sleep(2);
656
+ value.$jazz.set("pet", "cat");
657
+
658
+ const valueOnReader = await schema.load(value.$jazz.id, {
659
+ loadAs: reader,
660
+ });
661
+ assert(valueOnReader.$isLoaded);
662
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
663
+
664
+ render(
665
+ <CoMapView
666
+ coValue={valueOnReader.$jazz.raw}
667
+ data={data}
668
+ node={reader.$jazz.localNode}
669
+ onNavigate={() => {}}
670
+ />,
671
+ );
672
+
673
+ const restoreButton = screen.getByTitle("Timeline");
674
+ fireEvent.click(restoreButton);
675
+
676
+ await waitFor(() => {
677
+ expect(screen.getByText("Select Timestamp")).toBeDefined();
678
+ });
679
+
680
+ expect(screen.queryByText("Restore")).toBeNull();
681
+ expect(screen.queryByRole("checkbox")).toBeNull();
682
+ });
683
+
684
+ it("should show restore buttons for writer account when multiple timestamps exist", async () => {
685
+ const writer = await createJazzTestAccount();
686
+ const group = co.group().create({ owner: account });
687
+ group.addMember(writer, "writer");
688
+
689
+ const schema = co.map({
690
+ pet: z.string(),
691
+ });
692
+
693
+ const value = schema.create({ pet: "dog" }, group);
694
+ await sleep(2);
695
+ value.$jazz.set("pet", "cat");
696
+
697
+ const valueOnWriter = await schema.load(value.$jazz.id, {
698
+ loadAs: writer,
699
+ });
700
+ assert(valueOnWriter.$isLoaded);
701
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
702
+
703
+ render(
704
+ <CoMapView
705
+ coValue={valueOnWriter.$jazz.raw}
706
+ data={data}
707
+ node={writer.$jazz.localNode}
708
+ onNavigate={() => {}}
709
+ />,
710
+ );
711
+
712
+ const restoreButton = screen.getByTitle("Timeline");
713
+ fireEvent.click(restoreButton);
714
+
715
+ await waitFor(() => {
716
+ expect(screen.getByText("Restore")).toBeDefined();
717
+ });
718
+
719
+ expect(screen.getByRole("checkbox")).toBeDefined();
720
+ });
721
+
722
+ it("should hide edit buttons in GridView for reader account", async () => {
723
+ const reader = await createJazzTestAccount();
724
+ const group = co.group().create({ owner: account });
725
+ group.addMember(reader, "reader");
726
+
727
+ const schema = co.map({
728
+ pet: z.string(),
729
+ age: z.number(),
730
+ });
731
+
732
+ const value = schema.create({ pet: "dog", age: 10 }, group);
733
+
734
+ const valueOnReader = await schema.load(value.$jazz.id, {
735
+ loadAs: reader,
736
+ });
737
+ assert(valueOnReader.$isLoaded);
738
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
739
+
740
+ render(
741
+ <CoMapView
742
+ coValue={valueOnReader.$jazz.raw}
743
+ data={data}
744
+ node={reader.$jazz.localNode}
745
+ onNavigate={() => {}}
746
+ />,
747
+ );
748
+
749
+ expect(screen.getByText("pet")).toBeDefined();
750
+ expect(screen.getByText("age")).toBeDefined();
751
+
752
+ const editButtons = screen.queryAllByLabelText("Edit");
753
+ const deleteButtons = screen.queryAllByLabelText("Delete");
754
+
755
+ expect(editButtons).toHaveLength(0);
756
+ expect(deleteButtons).toHaveLength(0);
757
+ });
758
+
759
+ it("should show edit buttons in GridView for writer account", async () => {
760
+ const writer = await createJazzTestAccount();
761
+ const group = co.group().create({ owner: account });
762
+ group.addMember(writer, "writer");
763
+
764
+ const schema = co.map({
765
+ pet: z.string(),
766
+ age: z.number(),
767
+ });
768
+
769
+ const value = schema.create({ pet: "dog", age: 10 }, group);
770
+
771
+ const valueOnWriter = await schema.load(value.$jazz.id, {
772
+ loadAs: writer,
773
+ });
774
+ assert(valueOnWriter.$isLoaded);
775
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
776
+
777
+ render(
778
+ <CoMapView
779
+ coValue={valueOnWriter.$jazz.raw}
780
+ data={data}
781
+ node={writer.$jazz.localNode}
782
+ onNavigate={() => {}}
783
+ />,
784
+ );
785
+
786
+ expect(screen.getByText("pet")).toBeDefined();
787
+ expect(screen.getByText("age")).toBeDefined();
788
+
789
+ const editButtons = screen.queryAllByLabelText("Edit");
790
+ const deleteButtons = screen.queryAllByLabelText("Delete");
791
+
792
+ expect(editButtons.length).toBeGreaterThan(0);
793
+ expect(deleteButtons.length).toBeGreaterThan(0);
794
+ });
795
+
796
+ it("should enable Add Property button for admin account", async () => {
797
+ const admin = await createJazzTestAccount();
798
+ const group = co.group().create({ owner: account });
799
+ group.addMember(admin, "admin");
800
+
801
+ const schema = co.map({
802
+ pet: z.string(),
803
+ });
804
+
805
+ const value = schema.create({ pet: "dog" }, group);
806
+
807
+ const valueOnAdmin = await schema.load(value.$jazz.id, {
808
+ loadAs: admin,
809
+ });
810
+ assert(valueOnAdmin.$isLoaded);
811
+ const data = valueOnAdmin.$jazz.raw.toJSON() as JsonObject;
812
+
813
+ render(
814
+ <CoMapView
815
+ coValue={valueOnAdmin.$jazz.raw}
816
+ data={data}
817
+ node={admin.$jazz.localNode}
818
+ onNavigate={() => {}}
819
+ />,
820
+ );
821
+
822
+ const addButton = screen.getByTitle("Add Property");
823
+ expect(addButton).toBeDefined();
824
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
825
+ });
826
+
827
+ it("should enable Add Property button for manager account", async () => {
828
+ const manager = await createJazzTestAccount();
829
+ const group = co.group().create({ owner: account });
830
+ group.addMember(manager, "manager");
831
+
832
+ const schema = co.map({
833
+ pet: z.string(),
834
+ });
835
+
836
+ const value = schema.create({ pet: "dog" }, group);
837
+
838
+ const valueOnManager = await schema.load(value.$jazz.id, {
839
+ loadAs: manager,
840
+ });
841
+ assert(valueOnManager.$isLoaded);
842
+ const data = valueOnManager.$jazz.raw.toJSON() as JsonObject;
843
+
844
+ render(
845
+ <CoMapView
846
+ coValue={valueOnManager.$jazz.raw}
847
+ data={data}
848
+ node={manager.$jazz.localNode}
849
+ onNavigate={() => {}}
850
+ />,
851
+ );
852
+
853
+ const addButton = screen.getByTitle("Add Property");
854
+ expect(addButton).toBeDefined();
855
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
856
+ });
857
+
858
+ it("should enable Add Property button for writeOnly account", async () => {
859
+ const writeOnly = await createJazzTestAccount();
860
+ const group = co.group().create({ owner: account });
861
+ group.addMember(writeOnly, "writeOnly");
862
+
863
+ const schema = co.map({
864
+ pet: z.string(),
865
+ });
866
+
867
+ const value = schema.create({ pet: "dog" }, group);
868
+
869
+ const valueOnWriteOnly = await schema.load(value.$jazz.id, {
870
+ loadAs: writeOnly,
871
+ });
872
+ assert(valueOnWriteOnly.$isLoaded);
873
+ const data = valueOnWriteOnly.$jazz.raw.toJSON() as JsonObject;
874
+
875
+ render(
876
+ <CoMapView
877
+ coValue={valueOnWriteOnly.$jazz.raw}
878
+ data={data}
879
+ node={writeOnly.$jazz.localNode}
880
+ onNavigate={() => {}}
881
+ />,
882
+ );
883
+
884
+ const addButton = screen.getByTitle("Add Property");
885
+ expect(addButton).toBeDefined();
886
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
887
+ });
888
+ });
581
889
  });
@@ -17,6 +17,8 @@ import { HistoryView } from "../../viewer/history-view";
17
17
  import { setup } from "goober";
18
18
  import React from "react";
19
19
 
20
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
21
+
20
22
  function extractAction(row: HTMLElement | null | undefined) {
21
23
  if (!row) return "";
22
24
  // index 0: author, index 1: action, index 2: timestamp
@@ -30,7 +32,6 @@ function extractActions(): string[] {
30
32
 
31
33
  describe("HistoryView", async () => {
32
34
  const account = await setupJazzTestSync();
33
- const account2 = await createJazzTestAccount();
34
35
 
35
36
  beforeAll(() => {
36
37
  // setup goober
@@ -270,7 +271,138 @@ describe("HistoryView", async () => {
270
271
  });
271
272
  });
272
273
 
273
- describe("co.group", () => {
274
+ describe("co.plaintext", () => {
275
+ it("should render co.plaintext initial append in a single row", async () => {
276
+ const value = co.plainText().create("hello", account);
277
+ render(
278
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
279
+ );
280
+
281
+ expect(extractActions()).toEqual(['"hello" has been appended']);
282
+ });
283
+
284
+ it("should render co.plaintext appends in a single row", async () => {
285
+ const value = co.plainText().create("hello", account);
286
+ value.$jazz.applyDiff("hello world");
287
+ value.$jazz.applyDiff("hello world!");
288
+
289
+ expect(value.$jazz.raw.toString()).toEqual("hello world!");
290
+
291
+ render(
292
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
293
+ );
294
+
295
+ const history = [
296
+ '"hello" has been appended',
297
+ '" world" has been inserted after "o"',
298
+ '"!" has been inserted after " "', // it is after " " because previous action is reversed
299
+ ].toReversed(); // Default sort is descending
300
+
301
+ expect(extractActions()).toEqual(history);
302
+ });
303
+
304
+ it("should render co.plaintext delete in tail", async () => {
305
+ const value = co.plainText().create("hello", account);
306
+ value.$jazz.applyDiff("hell");
307
+
308
+ expect(value.$jazz.raw.toString()).toEqual("hell");
309
+
310
+ render(
311
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
312
+ );
313
+
314
+ const history = [
315
+ '"hello" has been appended',
316
+ '"o" has been deleted',
317
+ ].toReversed(); // Default sort is descending
318
+
319
+ expect(extractActions()).toEqual(history);
320
+ });
321
+
322
+ it("should render co.plaintext delete in head", async () => {
323
+ const value = co.plainText().create("hello", account);
324
+ value.$jazz.applyDiff("ello");
325
+
326
+ expect(value.$jazz.raw.toString()).toEqual("ello");
327
+
328
+ render(
329
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
330
+ );
331
+
332
+ const history = [
333
+ '"hello" has been appended',
334
+ '"h" has been deleted',
335
+ ].toReversed(); // Default sort is descending
336
+
337
+ expect(extractActions()).toEqual(history);
338
+ });
339
+
340
+ it("should render co.plaintext delete history of multiple old insertions in a single row", async () => {
341
+ const value = co.plainText().create("hello", account);
342
+ await sleep(2);
343
+ value.$jazz.applyDiff("hello world");
344
+ await sleep(2);
345
+ value.$jazz.applyDiff("hed");
346
+
347
+ expect(value.$jazz.raw.toString()).toEqual("hed");
348
+
349
+ render(
350
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
351
+ );
352
+
353
+ const history = [
354
+ '"hello" has been appended',
355
+ '" world" has been inserted after "o"',
356
+ '"lod" has been deleted',
357
+ '" worl" has been deleted',
358
+ ].toReversed(); // Default sort is descending
359
+
360
+ expect(extractActions()).toEqual(history);
361
+ });
362
+
363
+ it("should render co.plaintext insertBefore in history", async () => {
364
+ const value = co.plainText().create("world", account);
365
+ await sleep(2);
366
+ value.insertBefore(0, "Hello, ");
367
+
368
+ expect(value.$jazz.raw.toString()).toEqual("Hello, world");
369
+
370
+ render(
371
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
372
+ );
373
+
374
+ const history = [
375
+ '"world" has been appended',
376
+ '"H" has been inserted before "w"',
377
+ '"ello, " has been inserted after "H"',
378
+ ].toReversed(); // Default sort is descending
379
+
380
+ expect(extractActions()).toEqual(history);
381
+ });
382
+
383
+ it("should render co.plaintext insertAfter in history", async () => {
384
+ const value = co.plainText().create("world", account);
385
+ await sleep(2);
386
+ value.insertAfter(0, "Hello, ");
387
+
388
+ expect(value.$jazz.raw.toString()).toEqual("wHello, orld");
389
+
390
+ render(
391
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
392
+ );
393
+
394
+ const history = [
395
+ '"world" has been appended',
396
+ '"Hello, " has been inserted after "w"',
397
+ ].toReversed(); // Default sort is descending
398
+
399
+ expect(extractActions()).toEqual(history);
400
+ });
401
+ });
402
+
403
+ describe("co.group", async () => {
404
+ const account2 = await createJazzTestAccount();
405
+
274
406
  it("should render co.group changes", async () => {
275
407
  const group = co.group().create(account);
276
408
 
@@ -1,5 +1,172 @@
1
- import type { JsonObject, JsonValue, RawCoMap, Role } from "cojson";
1
+ import type {
2
+ JsonObject,
3
+ JsonValue,
4
+ OpID,
5
+ RawCoMap,
6
+ RawCoPlainText,
7
+ RawCoValue,
8
+ Role,
9
+ } from "cojson";
10
+ import { stringifyOpID } from "cojson";
11
+ import type { VerifiedTransaction } from "cojson/dist/coValueCore/coValueCore.js";
2
12
  import type { MapOpPayload } from "cojson/dist/coValues/coMap.js";
13
+ import * as TransactionChanges from "./transactions-changes";
14
+ import type {
15
+ DeletionOpPayload,
16
+ InsertionOpPayload,
17
+ } from "cojson/dist/coValues/coList.js";
18
+
19
+ export function areSameOpIds(
20
+ opId1: OpID | string,
21
+ opId2: OpID | string,
22
+ ): boolean {
23
+ if (typeof opId1 === "string" || typeof opId2 === "string") {
24
+ return opId1 === opId2;
25
+ }
26
+
27
+ return (
28
+ opId1.sessionID === opId2.sessionID &&
29
+ opId1.txIndex === opId2.txIndex &&
30
+ opId1.changeIdx === opId2.changeIdx
31
+ );
32
+ }
33
+
34
+ export function isCoPlainText(coValue: RawCoValue): coValue is RawCoPlainText {
35
+ return coValue.type === "coplaintext";
36
+ }
37
+
38
+ export function getTransactionChanges(
39
+ tx: VerifiedTransaction,
40
+ coValue: RawCoValue,
41
+ ): JsonValue[] {
42
+ if (tx.isValid === false && tx.tx.privacy === "private") {
43
+ const readKey = coValue.core.getReadKey(tx.tx.keyUsed);
44
+ if (!readKey) {
45
+ return [
46
+ `Unable to decrypt transaction: read key ${tx.tx.keyUsed} not found.`,
47
+ ];
48
+ }
49
+
50
+ return (
51
+ coValue.core.verified.decryptTransaction(
52
+ tx.txID.sessionID,
53
+ tx.txID.txIndex,
54
+ readKey,
55
+ ) ?? []
56
+ );
57
+ }
58
+
59
+ // Trying to collapse multiple changes into a single action in the history
60
+ if (isCoPlainText(coValue)) {
61
+ if (tx.changes === undefined || tx.changes.length === 0) return [];
62
+ const firstChange = tx.changes[0]!;
63
+
64
+ if (
65
+ TransactionChanges.isItemAppend(firstChange) &&
66
+ tx.changes.every(
67
+ (c) =>
68
+ TransactionChanges.isItemAppend(c) &&
69
+ areSameOpIds(c.after, firstChange.after),
70
+ )
71
+ ) {
72
+ const changes = tx.changes as InsertionOpPayload<string>[];
73
+ if (firstChange.after !== "start") {
74
+ changes.reverse();
75
+ }
76
+
77
+ return [
78
+ {
79
+ op: "app",
80
+ value: changes.map((c) => c.value).join(""),
81
+ after: firstChange.after,
82
+ },
83
+ ];
84
+ }
85
+
86
+ if (
87
+ TransactionChanges.isItemPrepend(firstChange) &&
88
+ tx.changes.every(
89
+ (c) =>
90
+ TransactionChanges.isItemPrepend(c) &&
91
+ areSameOpIds(c.before, firstChange.before),
92
+ )
93
+ ) {
94
+ const changes = tx.changes as InsertionOpPayload<string>[];
95
+ if (firstChange.before !== "end") {
96
+ changes.reverse();
97
+ }
98
+
99
+ return [
100
+ {
101
+ op: "pre",
102
+ value: changes.map((c) => c.value).join(""),
103
+ before: firstChange.before,
104
+ },
105
+ ];
106
+ }
107
+
108
+ if (
109
+ TransactionChanges.isItemDeletion(firstChange) &&
110
+ tx.changes.every((c) => TransactionChanges.isItemDeletion(c))
111
+ ) {
112
+ const coValueBeforeDeletions = coValue.atTime(tx.madeAt - 1);
113
+
114
+ // Verify if the deleted chars are consecutive
115
+ function changesAreConsecutive(changes: DeletionOpPayload[]): boolean {
116
+ if (changes.length < 2) return false;
117
+ const mapping = coValueBeforeDeletions.mapping.idxAfterOpID;
118
+
119
+ for (let i = 1; i < changes.length; ++i) {
120
+ const prevIdx = mapping[stringifyOpID(changes[i - 1]!.insertion)];
121
+ const currIdx = mapping[stringifyOpID(changes[i]!.insertion)];
122
+ if (currIdx !== prevIdx && currIdx !== (prevIdx ?? -2) + 1) {
123
+ return false;
124
+ }
125
+ }
126
+ return true;
127
+ }
128
+
129
+ if (changesAreConsecutive(tx.changes)) {
130
+ // Group the deletions by insertion.sessionID-txIndex
131
+ // This is to help the readability of deletions that act on different previous transactions
132
+ const groupedBySession: Map<string, DeletionOpPayload[]> = new Map();
133
+ for (const change of tx.changes) {
134
+ const group = `${change.insertion.sessionID}-${change.insertion.txIndex}`;
135
+ if (!groupedBySession.has(group)) groupedBySession.set(group, []);
136
+ groupedBySession.get(group)!.push(change);
137
+ }
138
+
139
+ return Array.from(groupedBySession.values()).map((changes) => {
140
+ const stringDeleted = changes
141
+ // order by txIndex and changeIdx
142
+ .toSorted((a, b) => {
143
+ if (a.insertion.txIndex === b.insertion.txIndex) {
144
+ return a.insertion.changeIdx - b.insertion.changeIdx;
145
+ }
146
+
147
+ return a.insertion.txIndex - b.insertion.txIndex;
148
+ })
149
+ // extract the single char from the insertions
150
+ .map((c) =>
151
+ coValueBeforeDeletions.get(
152
+ coValueBeforeDeletions.mapping.idxAfterOpID[
153
+ stringifyOpID(c.insertion)
154
+ ]!,
155
+ ),
156
+ )
157
+ .join("");
158
+
159
+ return {
160
+ op: "custom",
161
+ action: `"${stringDeleted}" has been deleted`,
162
+ };
163
+ });
164
+ }
165
+ }
166
+ }
167
+
168
+ return tx.changes ?? (tx.tx as any).changes ?? [];
169
+ }
3
170
 
4
171
  export function restoreCoMapToTimestamp(
5
172
  coValue: RawCoMap,
@@ -0,0 +1,10 @@
1
+ import { Role } from "cojson";
2
+
3
+ export function isWriter(role: Role | undefined): boolean {
4
+ return (
5
+ role === "writer" ||
6
+ role === "admin" ||
7
+ role === "manager" ||
8
+ role === "writeOnly"
9
+ );
10
+ }