@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.
- package/dist/bundle/index.js +9 -7
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/__tests__/entities.test.js +438 -5
- package/dist/esm/__tests__/entities.test.js.map +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -0
- package/dist/esm/__tests__/fixtures/testStorage.js +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/client/Client.js +2 -1
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/entities/Entity.d.ts +22 -3
- package/dist/esm/entities/Entity.js +210 -38
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/Entity.test.js +4 -3
- package/dist/esm/entities/Entity.test.js.map +1 -1
- package/dist/esm/entities/EntityCache.d.ts +6 -4
- package/dist/esm/entities/EntityCache.js +18 -7
- package/dist/esm/entities/EntityCache.js.map +1 -1
- package/dist/esm/entities/EntityMetadata.d.ts +8 -2
- package/dist/esm/entities/EntityMetadata.js +52 -4
- package/dist/esm/entities/EntityMetadata.js.map +1 -1
- package/dist/esm/entities/EntityStore.d.ts +4 -4
- package/dist/esm/entities/EntityStore.js +8 -10
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/OperationBatcher.d.ts +1 -1
- package/dist/esm/entities/OperationBatcher.js +2 -0
- package/dist/esm/entities/OperationBatcher.js.map +1 -1
- package/dist/esm/files/EntityFile.d.ts +6 -1
- package/dist/esm/files/EntityFile.js +11 -4
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.d.ts +3 -1
- package/dist/esm/files/FileManager.js +2 -2
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/utils/versions.js +1 -1
- package/package.json +2 -2
- package/src/__tests__/entities.test.ts +471 -5
- package/src/__tests__/fixtures/testStorage.ts +1 -1
- package/src/client/Client.ts +6 -1
- package/src/entities/Entity.test.ts +8 -7
- package/src/entities/Entity.ts +239 -29
- package/src/entities/EntityCache.ts +20 -10
- package/src/entities/EntityMetadata.ts +69 -9
- package/src/entities/EntityStore.ts +15 -12
- package/src/entities/OperationBatcher.ts +16 -2
- package/src/files/EntityFile.ts +16 -3
- package/src/files/FileManager.ts +7 -3
- 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":"
|
|
1
|
+
{"version":3,"file":"FileManager.js","sourceRoot":"","sources":["../../../src/files/FileManager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verdant-web/store",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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 {
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
});
|
package/src/client/Client.ts
CHANGED
|
@@ -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(
|
|
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',
|