@verdant-web/store 4.2.0 → 4.4.0

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 (46) hide show
  1. package/dist/bundle/index.js +9 -7
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/esm/__tests__/entities.test.js +438 -5
  4. package/dist/esm/__tests__/entities.test.js.map +1 -1
  5. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -0
  6. package/dist/esm/__tests__/fixtures/testStorage.js +1 -1
  7. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  8. package/dist/esm/client/Client.js +2 -1
  9. package/dist/esm/client/Client.js.map +1 -1
  10. package/dist/esm/entities/Entity.d.ts +22 -3
  11. package/dist/esm/entities/Entity.js +210 -38
  12. package/dist/esm/entities/Entity.js.map +1 -1
  13. package/dist/esm/entities/Entity.test.js +4 -3
  14. package/dist/esm/entities/Entity.test.js.map +1 -1
  15. package/dist/esm/entities/EntityCache.d.ts +6 -4
  16. package/dist/esm/entities/EntityCache.js +18 -7
  17. package/dist/esm/entities/EntityCache.js.map +1 -1
  18. package/dist/esm/entities/EntityMetadata.d.ts +8 -2
  19. package/dist/esm/entities/EntityMetadata.js +52 -4
  20. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  21. package/dist/esm/entities/EntityStore.d.ts +4 -4
  22. package/dist/esm/entities/EntityStore.js +8 -10
  23. package/dist/esm/entities/EntityStore.js.map +1 -1
  24. package/dist/esm/entities/OperationBatcher.d.ts +1 -1
  25. package/dist/esm/entities/OperationBatcher.js +2 -0
  26. package/dist/esm/entities/OperationBatcher.js.map +1 -1
  27. package/dist/esm/files/EntityFile.d.ts +6 -1
  28. package/dist/esm/files/EntityFile.js +11 -4
  29. package/dist/esm/files/EntityFile.js.map +1 -1
  30. package/dist/esm/files/FileManager.d.ts +3 -1
  31. package/dist/esm/files/FileManager.js +2 -2
  32. package/dist/esm/files/FileManager.js.map +1 -1
  33. package/dist/esm/utils/versions.js +1 -1
  34. package/package.json +2 -2
  35. package/src/__tests__/entities.test.ts +471 -5
  36. package/src/__tests__/fixtures/testStorage.ts +1 -1
  37. package/src/client/Client.ts +6 -1
  38. package/src/entities/Entity.test.ts +8 -7
  39. package/src/entities/Entity.ts +239 -29
  40. package/src/entities/EntityCache.ts +20 -10
  41. package/src/entities/EntityMetadata.ts +69 -9
  42. package/src/entities/EntityStore.ts +15 -12
  43. package/src/entities/OperationBatcher.ts +16 -2
  44. package/src/files/EntityFile.ts +16 -3
  45. package/src/files/FileManager.ts +7 -3
  46. package/src/utils/versions.ts +1 -1
@@ -4,11 +4,11 @@ export class FileManager extends Disposable {
4
4
  constructor({ sync, context }) {
5
5
  super();
6
6
  this.cache = new Map();
7
- this.add = async (file) => {
7
+ this.add = async (file, parent) => {
8
8
  // immediately cache the file
9
9
  let entityFile = this.cache.get(file.id);
10
10
  if (!entityFile) {
11
- entityFile = new EntityFile(file.id, { ctx: this.context });
11
+ entityFile = new EntityFile(file.id, { ctx: this.context, parent });
12
12
  this.cache.set(file.id, entityFile);
13
13
  }
14
14
  if (!file.remote) {
@@ -1 +1 @@
1
- {"version":3,"file":"FileManager.js","sourceRoot":"","sources":["../../../src/files/FileManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileManager.js","sourceRoot":"","sources":["../../../src/files/FileManager.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@ export function getLatestVersion(data) {
10
10
  return tsVersion;
11
11
  }
12
12
  return v;
13
- }, 0);
13
+ }, 1);
14
14
  return latestVersion;
15
15
  }
16
16
  //# sourceMappingURL=versions.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdant-web/store",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "access": "public",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -34,7 +34,7 @@
34
34
  "jszip": "^3.10.1",
35
35
  "jwt-decode": "^3.1.2",
36
36
  "weak-event": "^2.0.5",
37
- "@verdant-web/common": "2.8.0"
37
+ "@verdant-web/common": "2.9.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "20.10.5",
@@ -1,7 +1,19 @@
1
- import { assert } from '@verdant-web/common';
2
- import { describe, it, expect, vi, MockedFunction, vitest } from 'vitest';
1
+ import {
2
+ assert,
3
+ createRef,
4
+ EventSubscriber,
5
+ FixedTimestampProvider,
6
+ NaiveTimestampProvider,
7
+ PatchCreator,
8
+ schema,
9
+ } from '@verdant-web/common';
10
+ import { describe, expect, it, MockedFunction, vi, vitest } from 'vitest';
11
+ import { WeakEvent } from 'weak-event';
12
+ import { Time } from '../context/Time.js';
13
+ import { EntityFamilyMetadata } from '../entities/EntityMetadata.js';
14
+ import { MARK_FAILED } from '../files/EntityFile.js';
15
+ import { Entity, EntityFile } from '../index.js';
3
16
  import { createTestStorage } from './fixtures/testStorage.js';
4
- import { EntityFile } from '../index.js';
5
17
 
6
18
  async function waitForStoragePropagation(mock: MockedFunction<any>) {
7
19
  await new Promise<void>((resolve, reject) => {
@@ -639,7 +651,7 @@ describe('entities', () => {
639
651
  content: { invalid: 'value' },
640
652
  });
641
653
  }).rejects.toThrowErrorMatchingInlineSnapshot(
642
- `[Error: Validation error: Expected string for field content, got [object Object]]`,
654
+ `[Error: Validation error: Expected string for field content, got {"invalid":"value"}]`,
643
655
  );
644
656
  });
645
657
 
@@ -650,7 +662,7 @@ describe('entities', () => {
650
662
  expect(() => {
651
663
  weird.set('file', { invalid: 'value' });
652
664
  }).toThrowErrorMatchingInlineSnapshot(
653
- `[Error: Validation error: Expected file or null for field file, got [object Object]]`,
665
+ `[Error: Validation error: Expected file or null for field file, got {"invalid":"value"}]`,
654
666
  );
655
667
 
656
668
  // valid options
@@ -705,4 +717,458 @@ describe('entities', () => {
705
717
 
706
718
  expect(item.deleted).toBe(true);
707
719
  });
720
+
721
+ it('should apply contextual changes to a pruned entity in a way consistent with the pruned view of the data', async () => {
722
+ // manually constructing an entity for this test is easiest,
723
+ // kind of hard to force invalid data otherwise
724
+ const onPendingOperations = vi.fn();
725
+ const time = new Time(new NaiveTimestampProvider(), 1);
726
+ // too much junk in here, have to manually pick and choose
727
+ let subId = 0;
728
+ const testCtx = {
729
+ globalEvents: new EventSubscriber(),
730
+ time,
731
+ log: vi.fn(),
732
+ patchCreator: new PatchCreator(
733
+ () => time.now,
734
+ () => `${subId++}`,
735
+ ),
736
+ files: {
737
+ add: vi.fn(),
738
+ },
739
+ weakRef: (v: any) => new WeakRef(v),
740
+ } as any;
741
+ const metadataFamily = new EntityFamilyMetadata({
742
+ ctx: testCtx,
743
+ onPendingOperations,
744
+ rootOid: 'foos/a',
745
+ });
746
+ const entity = new Entity({
747
+ oid: 'foos/a',
748
+ schema: {
749
+ type: 'object',
750
+ properties: {
751
+ id: schema.fields.id(),
752
+ content: schema.fields.string(),
753
+ items: schema.fields.array({
754
+ items: schema.fields.object({
755
+ properties: {
756
+ content: schema.fields.string(),
757
+ },
758
+ }),
759
+ }),
760
+ },
761
+ },
762
+ ctx: testCtx,
763
+ deleteSelf: vi.fn(),
764
+ files: {
765
+ add: vi.fn(),
766
+ } as any,
767
+ metadataFamily,
768
+ storeEvents: {
769
+ add: new WeakEvent(),
770
+ replace: new WeakEvent(),
771
+ resetAll: new WeakEvent(),
772
+ },
773
+ readonlyKeys: ['id'],
774
+ });
775
+ metadataFamily.addConfirmedData({
776
+ baselines: [
777
+ {
778
+ oid: 'foos/a:1',
779
+ snapshot: { content: 'item 1' },
780
+ timestamp: time.now,
781
+ },
782
+ {
783
+ oid: 'foos/a:2',
784
+ snapshot: { content: 'item 2' },
785
+ timestamp: time.now,
786
+ },
787
+ {
788
+ oid: 'foos/a:3',
789
+ snapshot: {}, // INVALID!
790
+ timestamp: time.now,
791
+ },
792
+ {
793
+ oid: 'foos/a:4',
794
+ snapshot: { content: 'item 4' },
795
+ timestamp: time.now,
796
+ },
797
+ {
798
+ oid: 'foos/a:5',
799
+ snapshot: [
800
+ createRef('foos/a:1'),
801
+ createRef('foos/a:2'),
802
+ createRef('foos/a:3'),
803
+ createRef('foos/a:4'),
804
+ ],
805
+ timestamp: time.now,
806
+ },
807
+ {
808
+ oid: 'foos/a',
809
+ snapshot: {
810
+ id: 'a',
811
+ content: 'the main foo',
812
+ items: createRef('foos/a:5'),
813
+ },
814
+ timestamp: time.now,
815
+ },
816
+ ],
817
+ });
818
+
819
+ expect(entity.deepInvalid).toBe(true);
820
+
821
+ // check all that worked, lol. and that it's
822
+ // pruned item 3
823
+ expect(entity.getSnapshot()).toEqual({
824
+ id: 'a',
825
+ content: 'the main foo',
826
+ items: [
827
+ { content: 'item 1' },
828
+ { content: 'item 2' },
829
+ { content: 'item 4' },
830
+ ],
831
+ });
832
+
833
+ // also check that unpruned snapshot is correct
834
+ expect(entity.getUnprunedSnapshot()).toEqual({
835
+ id: 'a',
836
+ content: 'the main foo',
837
+ items: [
838
+ { content: 'item 1' },
839
+ { content: 'item 2' },
840
+ {},
841
+ { content: 'item 4' },
842
+ ],
843
+ });
844
+
845
+ // now, let's set the content of index 2.
846
+ // if this works as expected, we should replace
847
+ // item 4 (even though technically it's at index 3)
848
+ // because we are respecting the user's intention.
849
+ const items = entity.get('items');
850
+ items.set(2, { content: 'new item' });
851
+
852
+ expect(entity.getSnapshot()).toEqual({
853
+ id: 'a',
854
+ content: 'the main foo',
855
+ items: [
856
+ { content: 'item 1' },
857
+ { content: 'item 2' },
858
+ { content: 'new item' },
859
+ ],
860
+ });
861
+
862
+ // now, the entity has been 'fixed' and is valid again
863
+ expect(entity.getSnapshot()).toEqual(entity.getUnprunedSnapshot());
864
+ expect(entity.deepInvalid).toBe(false);
865
+ });
866
+
867
+ it('should only fire deep change once for a file change even if you get it multiple times', async () => {
868
+ // addressing a specific bug / lazy thing I did that caused a listener leak
869
+ const storage = await createTestStorage();
870
+ const weird = await storage.weirds.put({});
871
+ const fileList = weird.get('fileList');
872
+ const file = new window.File(['d(⌐□_□)b'], 'test.txt', {
873
+ type: 'text/plain',
874
+ });
875
+ fileList.push(file);
876
+ const deepListener = vi.fn();
877
+ fileList.subscribe('changeDeep', deepListener);
878
+ fileList.get(0);
879
+ fileList.get(0);
880
+ fileList.get(0);
881
+ // easy way to trigger change
882
+ fileList.get(0)[MARK_FAILED]();
883
+ expect(deepListener).toHaveBeenCalledTimes(1);
884
+ });
885
+
886
+ it('should correctly prune deep nested objects', () => {
887
+ // manually constructing an entity for this test is easiest,
888
+ // kind of hard to force invalid data otherwise
889
+ const onPendingOperations = vi.fn();
890
+ const time = new Time(new FixedTimestampProvider(), 1);
891
+ // too much junk in here, have to manually pick and choose
892
+ let subId = 100; // avoid false-flag matching of other ids by choosing an arbitrary start point
893
+ const testCtx = {
894
+ globalEvents: new EventSubscriber(),
895
+ time,
896
+ log: console.log,
897
+ patchCreator: new PatchCreator(
898
+ () => time.now,
899
+ () => `${subId++}`,
900
+ ),
901
+ files: {
902
+ add: vi.fn(),
903
+ },
904
+ weakRef: (v: any) => new WeakRef(v),
905
+ } as any;
906
+ const metadataFamily = new EntityFamilyMetadata({
907
+ ctx: testCtx,
908
+ onPendingOperations,
909
+ rootOid: 'foos/a',
910
+ });
911
+ const entity = new Entity({
912
+ oid: 'foos/a',
913
+ schema: {
914
+ type: 'object',
915
+ properties: {
916
+ id: schema.fields.id(),
917
+ // should prune to an empty list entity
918
+ listTest: schema.fields.array({
919
+ items: schema.fields.object({
920
+ fields: {
921
+ required: schema.fields.string(),
922
+ },
923
+ }),
924
+ }),
925
+ objectTest: schema.fields.object({
926
+ default: () => ({
927
+ // @ts-ignore - excessive nesting
928
+ nested: {
929
+ required: 'foo',
930
+ },
931
+ }),
932
+ fields: {
933
+ nested: schema.fields.object({
934
+ fields: {
935
+ required: schema.fields.string(),
936
+ },
937
+ }),
938
+ },
939
+ }),
940
+ // this one will not be present at all.
941
+ missingTest: schema.fields.object({
942
+ default: () => ({
943
+ value: 1,
944
+ }),
945
+ fields: {
946
+ value: schema.fields.number(),
947
+ },
948
+ }),
949
+ },
950
+ },
951
+ ctx: testCtx,
952
+ deleteSelf: vi.fn(),
953
+ files: {
954
+ add: vi.fn(),
955
+ } as any,
956
+ metadataFamily,
957
+ storeEvents: {
958
+ add: new WeakEvent(),
959
+ replace: new WeakEvent(),
960
+ resetAll: new WeakEvent(),
961
+ },
962
+ readonlyKeys: ['id'],
963
+ });
964
+ metadataFamily.addConfirmedData({
965
+ baselines: [
966
+ {
967
+ // an invalid list item
968
+ oid: 'foos/a:1',
969
+ snapshot: {},
970
+ timestamp: time.now,
971
+ },
972
+ {
973
+ oid: 'foos/a:2',
974
+ snapshot: [createRef('foos/a:1')],
975
+ timestamp: time.now,
976
+ },
977
+ {
978
+ // an invalid nested object
979
+ oid: 'foos/a:3',
980
+ snapshot: {},
981
+ timestamp: time.now,
982
+ },
983
+ {
984
+ oid: 'foos/a:4',
985
+ snapshot: { nested: createRef('foos/a:3') },
986
+ timestamp: time.now,
987
+ },
988
+ {
989
+ oid: 'foos/a',
990
+ snapshot: {
991
+ id: 'a',
992
+ listTest: createRef('foos/a:2'),
993
+ objectTest: createRef('foos/a:4'),
994
+ },
995
+ timestamp: time.now,
996
+ },
997
+ ],
998
+ });
999
+
1000
+ expect(entity.deepInvalid).toBe(true);
1001
+ expect(entity.getSnapshot()).toEqual({
1002
+ id: 'a',
1003
+ listTest: [],
1004
+ // gets the default, pruned
1005
+ objectTest: {
1006
+ nested: {
1007
+ required: 'foo',
1008
+ },
1009
+ },
1010
+ missingTest: {
1011
+ value: 1,
1012
+ },
1013
+ });
1014
+
1015
+ // the kicker...
1016
+ const list = entity.get('listTest');
1017
+ expect(list).toBeInstanceOf(Entity);
1018
+ const obj = entity.get('objectTest');
1019
+ expect(obj).toBeInstanceOf(Entity);
1020
+ const missing = entity.get('missingTest');
1021
+ expect(missing).toBeInstanceOf(Entity);
1022
+
1023
+ // we should be able to make edits to the pruned versions
1024
+ // and they get committed.
1025
+ list.push({ required: 'foo' });
1026
+ expect(entity.getSnapshot()).toEqual({
1027
+ id: 'a',
1028
+ listTest: [{ required: 'foo' }],
1029
+ objectTest: {
1030
+ nested: {
1031
+ required: 'foo',
1032
+ },
1033
+ },
1034
+ missingTest: {
1035
+ value: 1,
1036
+ },
1037
+ });
1038
+ obj.get('nested').set('required', 'bar');
1039
+ expect(entity.getSnapshot()).toEqual({
1040
+ id: 'a',
1041
+ listTest: [{ required: 'foo' }],
1042
+ objectTest: {
1043
+ nested: {
1044
+ required: 'bar',
1045
+ },
1046
+ },
1047
+ missingTest: {
1048
+ value: 1,
1049
+ },
1050
+ });
1051
+ missing.set('value', 2);
1052
+ expect(entity.getSnapshot()).toEqual({
1053
+ id: 'a',
1054
+ listTest: [{ required: 'foo' }],
1055
+ objectTest: {
1056
+ nested: {
1057
+ required: 'bar',
1058
+ },
1059
+ // gets the default, pruned
1060
+ },
1061
+ missingTest: {
1062
+ value: 2,
1063
+ },
1064
+ });
1065
+
1066
+ expect(onPendingOperations).toHaveBeenCalledTimes(5);
1067
+ // when doing those changes above, we see pending operations
1068
+ // added to construct the pruned versions with applied changes
1069
+ function removeExtra(obj: any) {
1070
+ delete obj.timestamp;
1071
+ delete obj.authz;
1072
+ return obj;
1073
+ }
1074
+ // being kind of precious about assertions here since
1075
+ // ordering is not quite as important and some ops create new
1076
+ // oids.
1077
+ const firstCall = onPendingOperations.mock.calls[0][0].map(removeExtra);
1078
+
1079
+ // the required field was set to the default
1080
+ expect(firstCall).toContainEqual({
1081
+ data: {
1082
+ name: 'required',
1083
+ op: 'set',
1084
+ value: 'foo',
1085
+ },
1086
+ oid: 'foos/a:3',
1087
+ });
1088
+
1089
+ // missingTest is created wholesale
1090
+ expect(firstCall).toContainEqual({
1091
+ data: {
1092
+ op: 'set',
1093
+ name: 'missingTest',
1094
+ value: {
1095
+ '@@type': 'ref',
1096
+ id: expect.stringMatching(/^foos\/a:/),
1097
+ },
1098
+ },
1099
+ oid: 'foos/a',
1100
+ });
1101
+ expect(firstCall).toContainEqual({
1102
+ data: {
1103
+ op: 'initialize',
1104
+ value: {
1105
+ value: 1,
1106
+ },
1107
+ },
1108
+ oid: expect.stringMatching(/^foos\/a:/),
1109
+ });
1110
+
1111
+ // we asserted all the contents, now also assert the length
1112
+ expect(firstCall).toHaveLength(3);
1113
+
1114
+ const secondCall = onPendingOperations.mock.calls[1][0].map(removeExtra);
1115
+ // the invalid item was removed from the array
1116
+ expect(secondCall).toContainEqual({
1117
+ data: {
1118
+ op: 'delete',
1119
+ },
1120
+ oid: 'foos/a:1',
1121
+ });
1122
+ expect(secondCall).toContainEqual({
1123
+ data: {
1124
+ op: 'list-remove',
1125
+ only: 'last',
1126
+ value: {
1127
+ '@@type': 'ref',
1128
+ id: 'foos/a:1',
1129
+ },
1130
+ },
1131
+ oid: 'foos/a:2',
1132
+ });
1133
+
1134
+ const thirdCall = onPendingOperations.mock.calls[2][0].map(removeExtra);
1135
+ // we can be more lax here
1136
+ expect(thirdCall).toMatchInlineSnapshot(`
1137
+ [
1138
+ {
1139
+ "data": {
1140
+ "op": "initialize",
1141
+ "value": {
1142
+ "required": "foo",
1143
+ },
1144
+ },
1145
+ "oid": "foos/a:100",
1146
+ },
1147
+ {
1148
+ "data": {
1149
+ "op": "list-push",
1150
+ "value": {
1151
+ "@@type": "ref",
1152
+ "id": "foos/a:100",
1153
+ },
1154
+ },
1155
+ "oid": "foos/a:2",
1156
+ },
1157
+ ]
1158
+ `);
1159
+
1160
+ const fourthCall = onPendingOperations.mock.calls[3][0].map(removeExtra);
1161
+ expect(fourthCall).toMatchInlineSnapshot(`
1162
+ [
1163
+ {
1164
+ "data": {
1165
+ "name": "required",
1166
+ "op": "set",
1167
+ "value": "bar",
1168
+ },
1169
+ "oid": "foos/a:3",
1170
+ },
1171
+ ]
1172
+ `);
1173
+ });
708
1174
  });
@@ -92,7 +92,7 @@ export const weirdCollection = schema.collection({
92
92
  },
93
93
  });
94
94
 
95
- const testSchema = schema({
95
+ export const testSchema = schema({
96
96
  version: 1,
97
97
  collections: {
98
98
  todos: todoCollection,
@@ -205,13 +205,18 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
205
205
  return await this._entities.addData(data);
206
206
  }
207
207
  } catch (err) {
208
- this.context.log('critical', 'Sync failed', err);
208
+ this.context.log(
209
+ 'critical',
210
+ 'Sync failed. To avoid data corruption, the client will now shut down.',
211
+ err,
212
+ );
209
213
  this.emit(
210
214
  'developerError',
211
215
  new Error('Sync failed, see logs or cause', {
212
216
  cause: err,
213
217
  }),
214
218
  );
219
+ await this.close();
215
220
  throw err;
216
221
  }
217
222
  };
@@ -1,16 +1,16 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { Entity } from './Entity.js';
3
- import { EntityFamilyMetadata } from './EntityMetadata.js';
4
- import { Context } from '../context/context.js';
5
- import { EntityStoreEvents } from './EntityStore.js';
6
- import { WeakEvent } from 'weak-event';
7
- import { FileManager } from '../files/FileManager.js';
8
1
  import {
9
2
  NaiveTimestampProvider,
10
3
  PatchCreator,
11
4
  groupPatchesByOid,
12
5
  } from '@verdant-web/common';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import { WeakEvent } from 'weak-event';
8
+ import { Context } from '../context/context.js';
13
9
  import { Time } from '../context/Time.js';
10
+ import { FileManager } from '../files/FileManager.js';
11
+ import { Entity } from './Entity.js';
12
+ import { EntityFamilyMetadata } from './EntityMetadata.js';
13
+ import { EntityStoreEvents } from './EntityStore.js';
14
14
 
15
15
  describe('Entity', () => {
16
16
  const schema = {
@@ -65,6 +65,7 @@ describe('Entity', () => {
65
65
  log: vi.fn(),
66
66
  time: new Time(time, 1),
67
67
  patchCreator,
68
+ weakRef: (v: any) => new WeakRef(v),
68
69
  };
69
70
  const entity = new Entity({
70
71
  oid: 'test/1',