umap-project 2.6.3__py3-none-any.whl → 2.7.0b0__py3-none-any.whl

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.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (104) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +64 -1
  3. umap/context_processors.py +1 -0
  4. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/cs_CZ/LC_MESSAGES/django.po +96 -92
  6. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  7. umap/locale/de/LC_MESSAGES/django.po +19 -18
  8. umap/locale/en/LC_MESSAGES/django.po +47 -43
  9. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/fr/LC_MESSAGES/django.po +51 -47
  11. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/pt/LC_MESSAGES/django.po +64 -60
  13. umap/management/commands/clean_tilelayer.py +152 -0
  14. umap/management/commands/purge_purgatory.py +28 -0
  15. umap/models.py +27 -2
  16. umap/settings/base.py +2 -0
  17. umap/static/umap/base.css +4 -4
  18. umap/static/umap/css/contextmenu.css +5 -0
  19. umap/static/umap/css/icon.css +7 -2
  20. umap/static/umap/img/16-white.svg +9 -2
  21. umap/static/umap/img/16.svg +3 -0
  22. umap/static/umap/img/source/16-white.svg +10 -3
  23. umap/static/umap/img/source/16.svg +4 -1
  24. umap/static/umap/js/modules/autocomplete.js +7 -3
  25. umap/static/umap/js/modules/browser.js +7 -1
  26. umap/static/umap/js/modules/caption.js +6 -1
  27. umap/static/umap/js/modules/data/features.js +176 -2
  28. umap/static/umap/js/modules/data/layer.js +31 -26
  29. umap/static/umap/js/modules/formatter.js +3 -2
  30. umap/static/umap/js/modules/global.js +2 -0
  31. umap/static/umap/js/modules/importers/communesfr.js +13 -1
  32. umap/static/umap/js/modules/permissions.js +123 -93
  33. umap/static/umap/js/modules/rendering/ui.js +37 -212
  34. umap/static/umap/js/modules/sync/engine.js +365 -14
  35. umap/static/umap/js/modules/sync/hlc.js +106 -0
  36. umap/static/umap/js/modules/sync/updaters.js +4 -4
  37. umap/static/umap/js/modules/sync/websocket.js +1 -1
  38. umap/static/umap/js/modules/ui/base.js +2 -2
  39. umap/static/umap/js/modules/ui/contextmenu.js +34 -17
  40. umap/static/umap/js/modules/urls.js +5 -1
  41. umap/static/umap/js/modules/utils.js +5 -1
  42. umap/static/umap/js/umap.controls.js +47 -47
  43. umap/static/umap/js/umap.core.js +3 -3
  44. umap/static/umap/js/umap.forms.js +3 -1
  45. umap/static/umap/js/umap.js +95 -112
  46. umap/static/umap/locale/br.js +13 -4
  47. umap/static/umap/locale/br.json +13 -4
  48. umap/static/umap/locale/ca.js +21 -12
  49. umap/static/umap/locale/ca.json +21 -12
  50. umap/static/umap/locale/cs_CZ.js +87 -78
  51. umap/static/umap/locale/cs_CZ.json +87 -78
  52. umap/static/umap/locale/de.js +17 -8
  53. umap/static/umap/locale/de.json +17 -8
  54. umap/static/umap/locale/en.js +9 -2
  55. umap/static/umap/locale/en.json +9 -2
  56. umap/static/umap/locale/eu.js +10 -3
  57. umap/static/umap/locale/eu.json +10 -3
  58. umap/static/umap/locale/fa_IR.js +11 -4
  59. umap/static/umap/locale/fa_IR.json +11 -4
  60. umap/static/umap/locale/fr.js +11 -4
  61. umap/static/umap/locale/fr.json +11 -4
  62. umap/static/umap/locale/hu.js +10 -3
  63. umap/static/umap/locale/hu.json +10 -3
  64. umap/static/umap/locale/pt.js +17 -8
  65. umap/static/umap/locale/pt.json +17 -8
  66. umap/static/umap/locale/pt_PT.js +13 -4
  67. umap/static/umap/locale/pt_PT.json +13 -4
  68. umap/static/umap/locale/zh_TW.js +13 -4
  69. umap/static/umap/locale/zh_TW.json +13 -4
  70. umap/static/umap/map.css +7 -22
  71. umap/static/umap/unittests/hlc.js +158 -0
  72. umap/static/umap/unittests/sync.js +321 -15
  73. umap/static/umap/unittests/utils.js +23 -0
  74. umap/static/umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js +111 -80
  75. umap/templates/umap/dashboard_menu.html +4 -2
  76. umap/templates/umap/js.html +0 -4
  77. umap/tests/integration/test_anonymous_owned_map.py +1 -0
  78. umap/tests/integration/test_basics.py +1 -1
  79. umap/tests/integration/test_circles_layer.py +12 -0
  80. umap/tests/integration/test_datalayer.py +5 -0
  81. umap/tests/integration/test_draw_polygon.py +17 -9
  82. umap/tests/integration/test_draw_polyline.py +12 -8
  83. umap/tests/integration/test_edit_datalayer.py +4 -6
  84. umap/tests/integration/test_edit_map.py +1 -1
  85. umap/tests/integration/test_import.py +5 -0
  86. umap/tests/integration/test_map.py +5 -0
  87. umap/tests/integration/test_owned_map.py +1 -1
  88. umap/tests/integration/test_view_polygon.py +12 -12
  89. umap/tests/integration/test_websocket_sync.py +65 -3
  90. umap/tests/test_clean_tilelayer.py +83 -0
  91. umap/tests/test_datalayer.py +24 -0
  92. umap/tests/test_map_views.py +1 -0
  93. umap/tests/test_purge_purgatory.py +25 -0
  94. umap/tests/test_websocket_server.py +22 -0
  95. umap/urls.py +5 -1
  96. umap/views.py +6 -3
  97. umap/websocket_server.py +130 -27
  98. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/METADATA +9 -9
  99. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/RECORD +102 -97
  100. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +0 -1
  101. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +0 -7
  102. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/WHEEL +0 -0
  103. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/entry_points.txt +0 -0
  104. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/licenses/LICENSE +0 -0
umap/static/umap/map.css CHANGED
@@ -592,17 +592,16 @@ ul.photon-autocomplete {
592
592
  left: 0;
593
593
  right: 0;
594
594
  height: 46px;
595
- background-color: var(--color-darkGray);
596
595
  padding: 0 10px;
597
596
  text-align: start;
598
597
  line-height: var(--control-size);
599
598
  cursor: auto;
600
599
  border-bottom: 1px solid #222;
601
600
  z-index: var(--zindex-panels);
602
- opacity: 0.98;
603
- color: #fff;
604
601
  display: flex;
605
602
  justify-content: space-between;
603
+ background-color: var(--background-color);
604
+ color: var(--text-color);
606
605
  }
607
606
  .umap-left-edit-toolbox,
608
607
  .umap-right-edit-toolbox {
@@ -820,7 +819,6 @@ a.umap-control-caption,
820
819
  .umap-browser .off .feature {
821
820
  display: none;
822
821
  }
823
- .umap-facet-search .formbox,
824
822
  .umap-browser .datalayer {
825
823
  margin-bottom: 2px;
826
824
  border-radius: 2px;
@@ -831,7 +829,7 @@ a.umap-control-caption,
831
829
  .umap-browser.dark .datalayer ul {
832
830
  border: 1px solid #232729;
833
831
  }
834
- .umap-browser h5, .umap-facet-search h5 {
832
+ .umap-browser h5 {
835
833
  margin-bottom: 0;
836
834
  overflow: hidden;
837
835
  padding-inline-start: 5px;
@@ -849,7 +847,7 @@ a.umap-control-caption,
849
847
  .umap-browser h5 span {
850
848
  margin-inline-start: 10px;
851
849
  }
852
- .umap-browser li, .umap-facet-search li {
850
+ .umap-browser li {
853
851
  padding: 2px 0;
854
852
  white-space: nowrap;
855
853
  overflow: hidden;
@@ -867,13 +865,15 @@ a.umap-control-caption,
867
865
  -moz-box-sizing:border-box;
868
866
  -webkit-box-sizing:border-box;
869
867
  box-sizing: border-box;
870
- background: none;
871
868
  display: inline-block;
872
869
  padding: 0;
873
870
  width: 24px;
874
871
  text-align: center;
875
872
  margin-inline-start: 5px;
876
873
  }
874
+ .umap-browser .marker .feature-color {
875
+ background: none;
876
+ }
877
877
  .umap-browser.dark .datalayer .feature-color {
878
878
  box-shadow: 0 0 2px 0 #999 inset;
879
879
  }
@@ -884,18 +884,6 @@ a.umap-control-caption,
884
884
  font-style: normal;
885
885
  font-weight: bold;
886
886
  }
887
- .umap-browser .polygon .feature-color,
888
- .umap-browser .polyline .feature-color {
889
- box-shadow: 0 0 2px 0 black inset;
890
- background-image: url('./img/24.svg');
891
- background-size: 500%;
892
- }
893
- .umap-browser .polyline .feature-color {
894
- background-position: -72px -23px;
895
- }
896
- .umap-browser .polygon .feature-color {
897
- background-position: -48px -25px;
898
- }
899
887
  .umap-browser .datalayer-toggle-list {
900
888
  float: inline-end;
901
889
  margin-inline-end: 5px;
@@ -1499,9 +1487,6 @@ span.popup-icon {
1499
1487
  .umap-main-edit-toolbox .umap-user {
1500
1488
  margin-inline-end: 10px;
1501
1489
  }
1502
- .umap-main-edit-toolbox .umap-user:after {
1503
- display: none;
1504
- }
1505
1490
  }
1506
1491
  @media all and (max-width: 640px) {
1507
1492
  .umap-main-edit-toolbox .umap-user {
@@ -0,0 +1,158 @@
1
+ import { describe, it } from 'mocha'
2
+ import sinon from 'sinon'
3
+
4
+ import pkg from 'chai'
5
+ const { expect } = pkg
6
+
7
+ import { HybridLogicalClock } from '../js/modules/sync/hlc.js'
8
+
9
+ describe('HybridLogicalClock', () => {
10
+ let clock
11
+
12
+ describe('#parse', () => {
13
+ it('should reject invalid values', () => {
14
+ clock = new HybridLogicalClock()
15
+ expect(() => clock.parse('invalid')).to.throw()
16
+ expect(() => clock.parse('123:456')).to.throw()
17
+ expect(() => clock.parse('123:456:789:000')).to.throw()
18
+ })
19
+
20
+ it('should parse correct values', () => {
21
+ clock = new HybridLogicalClock()
22
+ const result = clock.parse('1625097600000:42:abc-123')
23
+ expect(result).to.deep.equal({
24
+ walltime: '1625097600000',
25
+ nn: 42,
26
+ id: 'abc-123',
27
+ })
28
+ })
29
+
30
+ it('should default to 0 for nn if none is provided', () => {
31
+ clock = new HybridLogicalClock()
32
+ const result = clock.parse('1625097600000::abc-123')
33
+ expect(result).to.deep.equal({
34
+ walltime: '1625097600000',
35
+ nn: 0,
36
+ id: 'abc-123',
37
+ })
38
+ })
39
+ })
40
+
41
+ describe('#serialize', () => {
42
+ it('should correctly serialize the clock', () => {
43
+ clock = new HybridLogicalClock(1625097600000, 42, 'abc-123')
44
+ expect(clock.serialize()).to.equal('1625097600000:42:abc-123')
45
+ })
46
+ })
47
+
48
+ describe('#tick', () => {
49
+ it('should increment walltime when current time is greater', () => {
50
+ const now = Date.now()
51
+ clock = new HybridLogicalClock(now - 1000, 0, 'test')
52
+ const result = clock.tick()
53
+ const parsed = clock.parse(result)
54
+ expect(parsed.walltime).to.be.at.least(now.toString())
55
+ expect(parsed.nn).to.equal(0)
56
+ })
57
+
58
+ it('should increment nn when current time is not greater', () => {
59
+ const now = Date.now()
60
+ clock = new HybridLogicalClock(now, 5, 'test')
61
+ sinon.useFakeTimers(now)
62
+ const result = clock.tick()
63
+ const parsed = clock.parse(result)
64
+ expect(parsed.walltime).to.equal(now.toString())
65
+ expect(parsed.nn).to.equal(6)
66
+ sinon.restore()
67
+ })
68
+ })
69
+
70
+ describe('#receive', () => {
71
+ it("should use current time when it's greater than both local and remote", () => {
72
+ const now = Date.now()
73
+ clock = new HybridLogicalClock(now - 1000, 0, 'local')
74
+ const result = clock.receive(`${now - 500}:0:remote`)
75
+ expect(result.walltime).to.be.at.least(now)
76
+ expect(result.nn).to.equal(0)
77
+ expect(result.id).to.equal('local')
78
+ })
79
+
80
+ it('should increment nn when local and remote times are equal', () => {
81
+ const now = Date.now()
82
+ clock = new HybridLogicalClock(now, 5, 'local')
83
+ const result = clock.receive(`${now}:7:remote`)
84
+ expect(result.walltime).to.equal(now)
85
+ expect(result.nn).to.equal(8)
86
+ expect(result.id).to.equal('local')
87
+ })
88
+
89
+ it('should use remote time and increment nn when remote time is greater', () => {
90
+ const now = Date.now()
91
+ clock = new HybridLogicalClock(now - 1000, 5, 'local')
92
+ const result = clock.receive(`${now}:7:remote`)
93
+ expect(result.walltime).to.be.least(now.toString())
94
+ expect(result.nn).to.equal(8)
95
+ expect(result.id).to.equal('local')
96
+ })
97
+
98
+ it('should increment local nn when local time is greater', () => {
99
+ const now = Date.now()
100
+ clock = new HybridLogicalClock(now, 5, 'local')
101
+ const result = clock.receive(`${now - 1000}:7:remote`)
102
+ expect(result.walltime).to.be.least(now)
103
+ expect(result.nn).to.equal(6)
104
+ expect(result.id).to.equal('local')
105
+ })
106
+ })
107
+
108
+ it('should maintain causal order across multiple operations', () => {
109
+ const hlc = new HybridLogicalClock()
110
+
111
+ // Simulate a sequence of events
112
+ const event1 = hlc.tick()
113
+
114
+ // Simulate some time passing
115
+ const clock = sinon.useFakeTimers(Date.now() + 100)
116
+
117
+ const event2 = hlc.tick()
118
+
119
+ // Simulate receiving a message from another node
120
+ const remoteEvent = hlc.receive(`${Date.now() - 50}:5:remote-id`)
121
+
122
+ const event3 = hlc.tick()
123
+
124
+ // Advance time significantly
125
+ clock.tick(1000)
126
+
127
+ const event4 = hlc.tick()
128
+
129
+ // Clean up the fake timer
130
+ clock.restore()
131
+
132
+ // Parse all events
133
+ const parsedEvent1 = hlc.parse(event1)
134
+ const parsedEvent2 = hlc.parse(event2)
135
+ const parsedEvent3 = hlc.parse(event3)
136
+ const parsedEvent4 = hlc.parse(event4)
137
+
138
+ // Assertions to ensure causal order is maintained
139
+ expect(parsedEvent2.walltime).to.be.greaterThan(parsedEvent1.walltime)
140
+ expect(parsedEvent3.walltime).to.equal(parsedEvent2.walltime)
141
+ expect(parsedEvent3.nn).to.be.greaterThan(parsedEvent2.nn)
142
+ expect(parsedEvent4.walltime).to.be.greaterThan(parsedEvent3.walltime)
143
+
144
+ // Check that all events have the same id
145
+ const uniqueIds = new Set([
146
+ parsedEvent1.id,
147
+ parsedEvent2.id,
148
+ parsedEvent3.id,
149
+ parsedEvent4.id,
150
+ ])
151
+ expect(uniqueIds.size).to.equal(1)
152
+
153
+ // Ensure we can compare events as strings and maintain the same order
154
+ const events = [event1, event2, event3, event4]
155
+ const sortedEvents = [...events].sort()
156
+ expect(sortedEvents).to.deep.equal(events)
157
+ })
158
+ })
@@ -5,10 +5,10 @@ import pkg from 'chai'
5
5
  const { expect } = pkg
6
6
 
7
7
  import { MapUpdater } from '../js/modules/sync/updaters.js'
8
- import { SyncEngine } from '../js/modules/sync/engine.js'
8
+ import { SyncEngine, Operations } from '../js/modules/sync/engine.js'
9
9
 
10
10
  describe('SyncEngine', () => {
11
- it('should initialize methods even before start', function () {
11
+ it('should initialize methods even before start', () => {
12
12
  const engine = new SyncEngine({})
13
13
  engine.upsert()
14
14
  engine.update()
@@ -16,8 +16,8 @@ describe('SyncEngine', () => {
16
16
  })
17
17
  })
18
18
 
19
- describe('#dispatch', function () {
20
- it('should raise an error on unknown updater', function () {
19
+ describe('#dispatch', () => {
20
+ it('should raise an error on unknown updater', () => {
21
21
  const dispatcher = new SyncEngine({})
22
22
  expect(() => {
23
23
  dispatcher.dispatch({
@@ -27,7 +27,7 @@ describe('#dispatch', function () {
27
27
  })
28
28
  }).to.throw(Error)
29
29
  })
30
- it('should produce an error on malformated messages', function () {
30
+ it('should produce an error on malformated messages', () => {
31
31
  const dispatcher = new SyncEngine({})
32
32
  expect(() => {
33
33
  dispatcher.dispatch({
@@ -36,7 +36,7 @@ describe('#dispatch', function () {
36
36
  })
37
37
  }).to.throw(Error)
38
38
  })
39
- it('should raise an unknown operations', function () {
39
+ it('should raise an unknown operations', () => {
40
40
  const dispatcher = new SyncEngine({})
41
41
  expect(() => {
42
42
  dispatcher.dispatch({
@@ -47,55 +47,55 @@ describe('#dispatch', function () {
47
47
  })
48
48
 
49
49
  describe('Updaters', () => {
50
- describe('BaseUpdater', function () {
50
+ describe('BaseUpdater', () => {
51
51
  let updater
52
52
  let map
53
53
  let obj
54
54
 
55
- this.beforeEach(function () {
55
+ beforeEach(() => {
56
56
  map = {}
57
57
  updater = new MapUpdater(map)
58
58
  obj = {}
59
59
  })
60
- it('should be able to set object properties', function () {
60
+ it('should be able to set object properties', () => {
61
61
  let obj = {}
62
62
  updater.updateObjectValue(obj, 'foo', 'foo')
63
63
  expect(obj).deep.equal({ foo: 'foo' })
64
64
  })
65
65
 
66
- it('should be able to set object properties recursively on existing objects', function () {
66
+ it('should be able to set object properties recursively on existing objects', () => {
67
67
  let obj = { foo: {} }
68
68
  updater.updateObjectValue(obj, 'foo.bar', 'foo')
69
69
  expect(obj).deep.equal({ foo: { bar: 'foo' } })
70
70
  })
71
71
 
72
- it('should be able to set object properties recursively on deep objects', function () {
72
+ it('should be able to set object properties recursively on deep objects', () => {
73
73
  let obj = { foo: { bar: { baz: {} } } }
74
74
  updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
75
75
  expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
76
76
  })
77
77
 
78
- it('should be able to replace object properties recursively on deep objects', function () {
78
+ it('should be able to replace object properties recursively on deep objects', () => {
79
79
  let obj = { foo: { bar: { baz: { test: 'test' } } } }
80
80
  updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
81
81
  expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
82
82
  })
83
83
 
84
- it('should not set object properties recursively on non-existing objects', function () {
84
+ it('should not set object properties recursively on non-existing objects', () => {
85
85
  let obj = { foo: {} }
86
86
  updater.updateObjectValue(obj, 'bar.bar', 'value')
87
87
 
88
88
  expect(obj).deep.equal({ foo: {} })
89
89
  })
90
90
 
91
- it('should delete keys for undefined values', function () {
91
+ it('should delete keys for undefined values', () => {
92
92
  let obj = { foo: 'foo' }
93
93
  updater.updateObjectValue(obj, 'foo', undefined)
94
94
 
95
95
  expect(obj).deep.equal({})
96
96
  })
97
97
 
98
- it('should delete keys for undefined values, recursively', function () {
98
+ it('should delete keys for undefined values, recursively', () => {
99
99
  let obj = { foo: { bar: 'bar' } }
100
100
  updater.updateObjectValue(obj, 'foo.bar', undefined)
101
101
 
@@ -103,3 +103,309 @@ describe('Updaters', () => {
103
103
  })
104
104
  })
105
105
  })
106
+
107
+ describe('Operations', () => {
108
+ describe('haveSameContext', () => {
109
+ const createOperation = (overrides = {}) => ({
110
+ subject: 'feature',
111
+ metadata: {
112
+ id: 'UxNjQ',
113
+ layerId: '606d26bd-230f-4d3e-a2a7-0c3caed71548',
114
+ featureType: 'marker',
115
+ },
116
+ ...overrides,
117
+ })
118
+
119
+ it('should check if subject and metadata are the same', () => {
120
+ const op1 = createOperation()
121
+ const op2 = createOperation()
122
+ const op3 = createOperation({
123
+ subject: 'datalayer',
124
+ metadata: { id: '606d26bd-230f-4d3e-a2a7-0c3caed71548' },
125
+ })
126
+
127
+ expect(Operations.haveSameContext(op1, op2)).to.be.true
128
+ expect(Operations.haveSameContext(op1, op3)).to.be.false
129
+ expect(Operations.haveSameContext(op2, op3)).to.be.false
130
+ })
131
+
132
+ it('should check if the key matches if there is any provided', () => {
133
+ const op1 = createOperation({ key: 'properties.name' })
134
+ const op2 = createOperation({ key: 'properties.name' })
135
+ const op3 = createOperation({ key: 'geometry' })
136
+ const op4 = createOperation()
137
+
138
+ expect(Operations.haveSameContext(op1, op2)).to.be.true
139
+ expect(Operations.haveSameContext(op1, op3)).to.be.false
140
+ expect(Operations.haveSameContext(op1, op4)).to.be.true
141
+ expect(Operations.haveSameContext(op4, createOperation())).to.be.true
142
+ })
143
+
144
+ it('should use deep equality for subject and metadata', () => {
145
+ const op1 = createOperation({ metadata: { nested: { value: 1 } } })
146
+ const op2 = createOperation({ metadata: { nested: { value: 1 } } })
147
+ const op3 = createOperation({ metadata: { nested: { value: 2 } } })
148
+
149
+ expect(Operations.haveSameContext(op1, op2)).to.be.true
150
+ expect(Operations.haveSameContext(op1, op3)).to.be.false
151
+ })
152
+ })
153
+
154
+ describe('sort', () => {
155
+ it('should sort operations by timestamp', () => {
156
+ const operations = [
157
+ { hlc: '1727193550:44:id1' },
158
+ { hlc: '1727193549:42:id1' },
159
+ { hlc: '1727193551:43:id1' },
160
+ ]
161
+ const sorted = Operations.sort(operations)
162
+ expect(sorted).to.deep.equal([
163
+ { hlc: '1727193549:42:id1' },
164
+ { hlc: '1727193550:44:id1' },
165
+ { hlc: '1727193551:43:id1' },
166
+ ])
167
+ })
168
+
169
+ it('should sort operations by NN when timestamp is the same', () => {
170
+ const operations = [
171
+ { hlc: '1727193549:42:id1' },
172
+ { hlc: '1727193549:44:id1' },
173
+ { hlc: '1727193549:43:id1' },
174
+ ]
175
+ const sorted = Operations.sort(operations)
176
+ expect(sorted).to.deep.equal([
177
+ { hlc: '1727193549:42:id1' },
178
+ { hlc: '1727193549:43:id1' },
179
+ { hlc: '1727193549:44:id1' },
180
+ ])
181
+ })
182
+
183
+ it('should sort operations by id if other fields are equal', () => {
184
+ const operations = [
185
+ { hlc: '1727193549:42:id3' },
186
+ { hlc: '1727193549:42:id2' },
187
+ { hlc: '1727193549:42:id1' },
188
+ ]
189
+ const sorted = Operations.sort(operations)
190
+ expect(sorted).to.deep.equal([
191
+ { hlc: '1727193549:42:id1' },
192
+ { hlc: '1727193549:42:id2' },
193
+ { hlc: '1727193549:42:id3' },
194
+ ])
195
+ })
196
+ })
197
+
198
+ describe('addLocal', () => {
199
+ it('should add a local operation with a new hlc', () => {
200
+ const ops = new Operations()
201
+ const inputMessage = { verb: 'update', subject: 'test' }
202
+ const result = ops.addLocal(inputMessage)
203
+ expect(result).to.have.property('hlc')
204
+ expect(result.hlc).to.match(/^\d+:\d+:[^:]+$/)
205
+ expect(result).to.include(inputMessage)
206
+ })
207
+ })
208
+
209
+ describe('sorted', () => {
210
+ it('should return sorted operations', () => {
211
+ const ops = new Operations()
212
+ ops._operations = [{ hlc: '1727193549:43:id1' }, { hlc: '1727193549:42:id1' }]
213
+ const sorted = ops.sorted()
214
+ expect(sorted[0].hlc).to.equal('1727193549:42:id1')
215
+ expect(sorted[1].hlc).to.equal('1727193549:43:id1')
216
+ })
217
+ })
218
+
219
+ describe('shouldBypassOperation', () => {
220
+ let ops
221
+
222
+ beforeEach(() => {
223
+ ops = new Operations()
224
+ })
225
+
226
+ const createOperation = (overrides = {}) => ({
227
+ verb: 'update',
228
+ subject: 'feature',
229
+ metadata: {
230
+ id: 'UxNjQ',
231
+ layerId: '606d26bd-230f-4d3e-a2a7-0c3caed71548',
232
+ featureType: 'marker',
233
+ },
234
+ key: 'properties.name',
235
+ value: 'default',
236
+ hlc: '0000000000000:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
237
+ ...overrides,
238
+ })
239
+
240
+ const createUpsertOperation = (overrides = {}) =>
241
+ createOperation({
242
+ verb: 'upsert',
243
+ key: undefined,
244
+ value: {
245
+ type: 'Feature',
246
+ geometry: {
247
+ coordinates: [0.439453, 48.04871],
248
+ type: 'Point',
249
+ },
250
+ properties: {},
251
+ id: 'UxNjQ',
252
+ },
253
+ ...overrides,
254
+ })
255
+
256
+ it('should return false if no local operation is newer', () => {
257
+ const remote = createUpsertOperation({ hlc: '1727184449050:44:id2' })
258
+ ops._operations = [
259
+ createOperation({
260
+ hlc: '1727184449010:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
261
+ }),
262
+ createUpsertOperation({
263
+ hlc: '1727184449020:0:b4a221a0-7b62-4588-a6af-041b041006dc',
264
+ }),
265
+ ]
266
+
267
+ const result = ops.shouldBypassOperation(remote)
268
+ expect(result).to.be.false
269
+ })
270
+
271
+ it('should return true if a similar "delete" operation is newer', () => {
272
+ const remote = createOperation({
273
+ verb: 'delete',
274
+ metadata: { id: 'M1NTA', layerId: '1234', featureType: 'marker' },
275
+ hlc: '1:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
276
+ })
277
+
278
+ ops._operations = [
279
+ createOperation({
280
+ verb: 'delete',
281
+ metadata: { id: 'M1NTA', layerId: '1234', featureType: 'marker' },
282
+ hlc: '2:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
283
+ }),
284
+ ]
285
+
286
+ const result = ops.shouldBypassOperation(remote)
287
+ expect(result).to.be.true
288
+ })
289
+
290
+ describe('update', () => {
291
+ it('should check for related updates', () => {
292
+ ops._operations = [
293
+ createOperation({
294
+ value: 'y',
295
+ hlc: '1:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
296
+ }),
297
+ createOperation({
298
+ value: 'youpi',
299
+ hlc: '9:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
300
+ }),
301
+ ]
302
+
303
+ const remoteOperation = createOperation({
304
+ value: 'something else',
305
+ hlc: '0:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
306
+ })
307
+
308
+ const result = ops.shouldBypassOperation(remoteOperation)
309
+ expect(result).to.be.true
310
+ })
311
+
312
+ it('should check for related deletes', () => {
313
+ ops._operations = [
314
+ {
315
+ verb: 'delete',
316
+ subject: 'feature',
317
+ metadata: {
318
+ id: 'M1NTA',
319
+ layerId: '123',
320
+ featureType: 'marker',
321
+ },
322
+ hlc: '1727196583562:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
323
+ key: undefined,
324
+ },
325
+ ]
326
+
327
+ const remoteOperation = createOperation({
328
+ metadata: { id: 'M1NTA', layerId: '123', featureType: 'marker' },
329
+ key: 'geometry',
330
+ value: { coordinates: [2.944336, 47.070122], type: 'Point' },
331
+ hlc: '0:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
332
+ })
333
+
334
+ const result = ops.shouldBypassOperation(remoteOperation)
335
+ expect(result).to.be.true
336
+ })
337
+ })
338
+
339
+ describe('upsert', () => {
340
+ it('should take precedence over updates (even if fresher)', () => {
341
+ ops._operations = [
342
+ createOperation({
343
+ value: 'youpi',
344
+ hlc: '1000000000000:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
345
+ }),
346
+ ]
347
+
348
+ const remoteOperation = createUpsertOperation({
349
+ hlc: '0000000000000:0:b4a221a0-7b62-4588-a6af-041b041006dc',
350
+ })
351
+
352
+ const result = ops.shouldBypassOperation(remoteOperation)
353
+ expect(result).to.be.false
354
+ })
355
+ })
356
+
357
+ describe('delete', () => {
358
+ it('should check for the same delete', () => {
359
+ ops._operations = [
360
+ createOperation({
361
+ verb: 'delete',
362
+ metadata: { id: 'I3MDg', layerId: null, featureType: 'polygon' },
363
+ key: undefined,
364
+ hlc: '1:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
365
+ }),
366
+ ]
367
+
368
+ const remoteOperation = createOperation({
369
+ verb: 'delete',
370
+ metadata: { id: 'I3MDg', layerId: null, featureType: 'polygon' },
371
+ key: undefined,
372
+ hlc: '0:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
373
+ })
374
+
375
+ const result = ops.shouldBypassOperation(remoteOperation)
376
+ expect(result).to.be.true
377
+ })
378
+ })
379
+ })
380
+ describe('storeRemoteOperations', () => {
381
+ it('should store remote operations and update the local HLC', () => {
382
+ const ops = new Operations()
383
+ const remoteOps = [{ hlc: '1727193549:42:id2' }, { hlc: '1727193549:43:id2' }]
384
+ ops.storeRemoteOperations(remoteOps)
385
+ expect(ops._operations).to.deep.equal(remoteOps)
386
+ })
387
+ })
388
+
389
+ describe('getOperationsSince', () => {
390
+ it('should return operations since a given HLC', () => {
391
+ const ops = new Operations()
392
+ ops._operations = [
393
+ { hlc: '1727193549:42:id1' },
394
+ { hlc: '1727193549:43:id1' },
395
+ { hlc: '1727193549:44:id1' },
396
+ ]
397
+ const result = ops.getOperationsSince('1727193549:42:id1')
398
+ expect(result).to.deep.equal([
399
+ { hlc: '1727193549:43:id1' },
400
+ { hlc: '1727193549:44:id1' },
401
+ ])
402
+ })
403
+
404
+ it('should return all operations if no HLC is provided', () => {
405
+ const ops = new Operations()
406
+ ops._operations = [{ hlc: '1727193549:42:id1' }, { hlc: '1727193549:43:id1' }]
407
+ const result = ops.getOperationsSince()
408
+ expect(result).to.deep.equal(ops._operations)
409
+ })
410
+ })
411
+ })
@@ -779,4 +779,27 @@ describe('Utils', () => {
779
779
  )
780
780
  })
781
781
  })
782
+
783
+ describe('#isObject', () => {
784
+ it('should return true for objects', () => {
785
+ assert.equal(Utils.isObject({}), true)
786
+ assert.equal(Utils.isObject({ foo: 'bar' }), true)
787
+ })
788
+
789
+ it('should return false for Array', () => {
790
+ assert.equal(Utils.isObject([]), false)
791
+ })
792
+
793
+ it('should return false on null', () => {
794
+ assert.equal(Utils.isObject(null), false)
795
+ })
796
+
797
+ it('should return false on undefined', () => {
798
+ assert.equal(Utils.isObject(undefined), false)
799
+ })
800
+
801
+ it('should return false on string', () => {
802
+ assert.equal(Utils.isObject(''), false)
803
+ })
804
+ })
782
805
  })