@powersync/service-core 1.8.1 → 1.10.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 (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/routes/configure-fastify.d.ts +1 -1
  6. package/dist/routes/endpoints/probes.d.ts +2 -2
  7. package/dist/routes/endpoints/probes.js +16 -2
  8. package/dist/routes/endpoints/probes.js.map +1 -1
  9. package/dist/storage/SyncRulesBucketStorage.d.ts +16 -5
  10. package/dist/storage/SyncRulesBucketStorage.js +2 -2
  11. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  12. package/dist/storage/WriteCheckpointAPI.d.ts +24 -2
  13. package/dist/storage/WriteCheckpointAPI.js.map +1 -1
  14. package/dist/storage/bson.d.ts +4 -3
  15. package/dist/storage/bson.js +6 -10
  16. package/dist/storage/bson.js.map +1 -1
  17. package/dist/{sync → streams}/BroadcastIterable.js +2 -2
  18. package/dist/streams/BroadcastIterable.js.map +1 -0
  19. package/dist/streams/Demultiplexer.d.ts +52 -0
  20. package/dist/streams/Demultiplexer.js +128 -0
  21. package/dist/streams/Demultiplexer.js.map +1 -0
  22. package/dist/{sync → streams}/LastValueSink.d.ts +2 -2
  23. package/dist/{sync → streams}/LastValueSink.js +2 -2
  24. package/dist/streams/LastValueSink.js.map +1 -0
  25. package/dist/{sync → streams}/merge.js +1 -1
  26. package/dist/streams/merge.js.map +1 -0
  27. package/dist/streams/streams-index.d.ts +4 -0
  28. package/dist/streams/streams-index.js +5 -0
  29. package/dist/streams/streams-index.js.map +1 -0
  30. package/dist/sync/BucketChecksumState.d.ts +7 -2
  31. package/dist/sync/BucketChecksumState.js +61 -40
  32. package/dist/sync/BucketChecksumState.js.map +1 -1
  33. package/dist/sync/sync-index.d.ts +0 -3
  34. package/dist/sync/sync-index.js +0 -3
  35. package/dist/sync/sync-index.js.map +1 -1
  36. package/dist/sync/sync.js +2 -2
  37. package/dist/sync/sync.js.map +1 -1
  38. package/dist/sync/util.d.ts +10 -1
  39. package/dist/sync/util.js +30 -0
  40. package/dist/sync/util.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/index.ts +3 -0
  43. package/src/routes/endpoints/probes.ts +18 -2
  44. package/src/storage/SyncRulesBucketStorage.ts +18 -7
  45. package/src/storage/WriteCheckpointAPI.ts +28 -2
  46. package/src/storage/bson.ts +10 -12
  47. package/src/{sync → streams}/BroadcastIterable.ts +2 -2
  48. package/src/streams/Demultiplexer.ts +165 -0
  49. package/src/{sync → streams}/LastValueSink.ts +2 -2
  50. package/src/{sync → streams}/merge.ts +1 -1
  51. package/src/streams/streams-index.ts +4 -0
  52. package/src/sync/BucketChecksumState.ts +71 -55
  53. package/src/sync/sync-index.ts +0 -3
  54. package/src/sync/sync.ts +2 -2
  55. package/src/sync/util.ts +34 -1
  56. package/test/src/broadcast_iterable.test.ts +8 -8
  57. package/test/src/demultiplexer.test.ts +205 -0
  58. package/test/src/merge_iterable.test.ts +1 -1
  59. package/test/src/routes/probes.integration.test.ts +5 -5
  60. package/test/src/routes/probes.test.ts +5 -4
  61. package/test/src/sync/BucketChecksumState.test.ts +14 -13
  62. package/test/src/util.test.ts +48 -0
  63. package/tsconfig.tsbuildinfo +1 -1
  64. package/dist/sync/BroadcastIterable.js.map +0 -1
  65. package/dist/sync/LastValueSink.js.map +0 -1
  66. package/dist/sync/merge.js.map +0 -1
  67. /package/dist/{sync → streams}/BroadcastIterable.d.ts +0 -0
  68. /package/dist/{sync → streams}/merge.d.ts +0 -0
@@ -0,0 +1,205 @@
1
+ // Vitest Unit Tests
2
+ import { Demultiplexer, DemultiplexerSource, DemultiplexerSourceFactory, DemultiplexerValue } from '@/index.js';
3
+ import { delayEach } from 'ix/asynciterable/operators/delayeach.js';
4
+ import { take } from 'ix/asynciterable/operators/take.js';
5
+ import { toArray } from 'ix/asynciterable/toarray.js';
6
+ import * as timers from 'node:timers/promises';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ describe('Demultiplexer', () => {
10
+ it('should start subscription lazily and provide first value', async () => {
11
+ const mockSource: DemultiplexerSourceFactory<string> = (signal: AbortSignal) => {
12
+ const iterator = (async function* (): AsyncIterable<DemultiplexerValue<string>> {})();
13
+ return {
14
+ iterator,
15
+ getFirstValue: async (key: string) => `first-${key}`
16
+ };
17
+ };
18
+
19
+ const demux = new Demultiplexer(mockSource);
20
+ const signal = new AbortController().signal;
21
+
22
+ const iter = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
23
+ const result = await iter.next();
24
+ expect(result.value).toBe('first-user1');
25
+ });
26
+
27
+ it('should handle multiple subscribers to the same key', async () => {
28
+ const iter = (async function* () {
29
+ yield { key: 'user1', value: 'value1' };
30
+ yield { key: 'user1', value: 'value2' };
31
+ })();
32
+ const source: DemultiplexerSource<string> = {
33
+ iterator: iter,
34
+ getFirstValue: async (key: string) => `first-${key}`
35
+ };
36
+
37
+ const demux = new Demultiplexer(() => source);
38
+ const signal = new AbortController().signal;
39
+
40
+ const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
41
+ const iter2 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
42
+
43
+ // Due to only keeping the last value, some values are skipped
44
+ expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
45
+ expect(await iter1.next()).toEqual({ value: 'value1', done: false });
46
+ expect(await iter1.next()).toEqual({ value: undefined, done: true });
47
+
48
+ expect(await iter2.next()).toEqual({ value: 'first-user1', done: false });
49
+ expect(await iter2.next()).toEqual({ value: undefined, done: true });
50
+ });
51
+
52
+ it('should handle multiple subscribers to the same key (2)', async () => {
53
+ const p1 = Promise.withResolvers<void>();
54
+ const p2 = Promise.withResolvers<void>();
55
+ const p3 = Promise.withResolvers<void>();
56
+
57
+ const iter = (async function* () {
58
+ await p1.promise;
59
+ yield { key: 'user1', value: 'value1' };
60
+ await p2.promise;
61
+ yield { key: 'user1', value: 'value2' };
62
+ await p3.promise;
63
+ })();
64
+
65
+ const source: DemultiplexerSource<string> = {
66
+ iterator: iter,
67
+ getFirstValue: async (key: string) => `first-${key}`
68
+ };
69
+
70
+ const demux = new Demultiplexer(() => source);
71
+ const signal = new AbortController().signal;
72
+
73
+ const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
74
+ const iter2 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
75
+
76
+ // Due to only keeping the last value, some values are skilled
77
+ expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
78
+ expect(await iter2.next()).toEqual({ value: 'first-user1', done: false });
79
+ p1.resolve();
80
+
81
+ expect(await iter1.next()).toEqual({ value: 'value1', done: false });
82
+ expect(await iter2.next()).toEqual({ value: 'value1', done: false });
83
+ p2.resolve();
84
+
85
+ expect(await iter1.next()).toEqual({ value: 'value2', done: false });
86
+ p3.resolve();
87
+
88
+ expect(await iter1.next()).toEqual({ value: undefined, done: true });
89
+ expect(await iter2.next()).toEqual({ value: undefined, done: true });
90
+ });
91
+
92
+ it('should handle multiple subscribers to different keys', async () => {
93
+ const p1 = Promise.withResolvers<void>();
94
+ const p2 = Promise.withResolvers<void>();
95
+ const p3 = Promise.withResolvers<void>();
96
+
97
+ const iter = (async function* () {
98
+ await p1.promise;
99
+ yield { key: 'user1', value: 'value1' };
100
+ await p2.promise;
101
+ yield { key: 'user2', value: 'value2' };
102
+ await p3.promise;
103
+ })();
104
+
105
+ const source: DemultiplexerSource<string> = {
106
+ iterator: iter,
107
+ getFirstValue: async (key: string) => `first-${key}`
108
+ };
109
+
110
+ const demux = new Demultiplexer(() => source);
111
+ const signal = new AbortController().signal;
112
+
113
+ const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
114
+ const iter2 = demux.subscribe('user2', signal)[Symbol.asyncIterator]();
115
+
116
+ // Due to only keeping the last value, some values are skilled
117
+ expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
118
+ expect(await iter2.next()).toEqual({ value: 'first-user2', done: false });
119
+ p1.resolve();
120
+
121
+ expect(await iter1.next()).toEqual({ value: 'value1', done: false });
122
+ p2.resolve();
123
+
124
+ expect(await iter2.next()).toEqual({ value: 'value2', done: false });
125
+ p3.resolve();
126
+
127
+ expect(await iter1.next()).toEqual({ value: undefined, done: true });
128
+ expect(await iter2.next()).toEqual({ value: undefined, done: true });
129
+ });
130
+
131
+ it('should abort', async () => {
132
+ const iter = (async function* () {
133
+ yield { key: 'user1', value: 'value1' };
134
+ yield { key: 'user1', value: 'value2' };
135
+ })();
136
+
137
+ const source: DemultiplexerSource<string> = {
138
+ iterator: iter,
139
+ getFirstValue: async (key: string) => `first-${key}`
140
+ };
141
+
142
+ const demux = new Demultiplexer(() => source);
143
+ const controller = new AbortController();
144
+
145
+ const iter1 = demux.subscribe('user1', controller.signal)[Symbol.asyncIterator]();
146
+
147
+ expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
148
+ controller.abort();
149
+
150
+ await expect(iter1.next()).rejects.toThrow('The operation has been aborted');
151
+ });
152
+
153
+ it('should handle errors on multiple subscribers', async () => {
154
+ let sourceIndex = 0;
155
+ const sourceFn = async function* (signal: AbortSignal): AsyncIterable<DemultiplexerValue<number>> {
156
+ // Test value out by 1000 means it may have used the wrong iteration of the source
157
+ const base = (sourceIndex += 1000);
158
+ const abortedPromise = new Promise((resolve) => {
159
+ signal.addEventListener('abort', resolve, { once: true });
160
+ });
161
+ for (let i = 0; !signal.aborted; i++) {
162
+ if (base + i == 1005) {
163
+ throw new Error('simulated failure');
164
+ }
165
+ yield { key: 'u1', value: base + i };
166
+ await Promise.race([abortedPromise, timers.setTimeout(1)]);
167
+ }
168
+ // Test value out by 100 means this wasn't reached
169
+ sourceIndex += 100;
170
+ };
171
+
172
+ const sourceFactory: DemultiplexerSourceFactory<number> = (signal) => {
173
+ const source: DemultiplexerSource<number> = {
174
+ iterator: sourceFn(signal),
175
+ getFirstValue: async (key: string) => -1
176
+ };
177
+ return source;
178
+ };
179
+ const demux = new Demultiplexer(sourceFactory);
180
+
181
+ const controller = new AbortController();
182
+
183
+ const delayed1 = delayEach(9)(demux.subscribe('u1', controller.signal));
184
+ const delayed2 = delayEach(10)(demux.subscribe('u1', controller.signal));
185
+ expect(demux.active).toBe(false);
186
+ const results1Promise = toArray(take(5)(delayed1)) as Promise<number[]>;
187
+ const results2Promise = toArray(take(5)(delayed2)) as Promise<number[]>;
188
+
189
+ const [r1, r2] = await Promise.allSettled([results1Promise, results2Promise]);
190
+
191
+ expect(r1).toEqual({ status: 'rejected', reason: new Error('simulated failure') });
192
+ expect(r2).toEqual({ status: 'rejected', reason: new Error('simulated failure') });
193
+
194
+ expect(demux.active).toBe(false);
195
+
196
+ // This starts a new source
197
+ const delayed3 = delayEach(10)(demux.subscribe('u1', controller.signal));
198
+ const results3 = await toArray(take(6)(delayed3));
199
+ expect(results3.length).toEqual(6);
200
+ expect(results3[0]).toEqual(-1); // Initial value
201
+ // There should be approximately 10ms between each value, but we allow for some slack
202
+ expect(results3[5]).toBeGreaterThan(2005);
203
+ expect(results3[5]).toBeLessThan(2200);
204
+ });
205
+ });
@@ -1,4 +1,4 @@
1
- import { mergeAsyncIterablesNew, mergeAsyncIterablesOld } from '@/sync/merge.js';
1
+ import { mergeAsyncIterablesNew, mergeAsyncIterablesOld } from '@/streams/merge.js';
2
2
  import * as timers from 'timers/promises';
3
3
  import { describe, expect, test } from 'vitest';
4
4
 
@@ -1,10 +1,10 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import Fastify, { FastifyInstance } from 'fastify';
3
1
  import { container } from '@powersync/lib-services-framework';
4
- import * as auth from '../../../src/routes/auth.js';
5
- import * as system from '../../../src/system/system-index.js';
2
+ import Fastify, { FastifyInstance } from 'fastify';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
4
  import { configureFastifyServer } from '../../../src/index.js';
5
+ import * as auth from '../../../src/routes/auth.js';
7
6
  import { ProbeRoutes } from '../../../src/routes/endpoints/probes.js';
7
+ import * as system from '../../../src/system/system-index.js';
8
8
 
9
9
  vi.mock('@powersync/lib-services-framework', async () => {
10
10
  const actual = (await vi.importActual('@powersync/lib-services-framework')) as any;
@@ -25,7 +25,7 @@ describe('Probe Routes Integration', () => {
25
25
 
26
26
  beforeEach(async () => {
27
27
  app = Fastify();
28
- mockSystem = { routerEngine: {} } as system.ServiceContext;
28
+ mockSystem = { routerEngine: {}, replicationEngine: {} } as system.ServiceContext;
29
29
  await configureFastifyServer(app, { service_context: mockSystem });
30
30
  await app.ready();
31
31
  });
@@ -1,6 +1,6 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
1
  import { container } from '@powersync/lib-services-framework';
3
- import { startupCheck, livenessCheck, readinessCheck } from '../../../src/routes/endpoints/probes.js';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { livenessCheck, readinessCheck, startupCheck } from '../../../src/routes/endpoints/probes.js';
4
4
 
5
5
  // Mock the container
6
6
  vi.mock('@powersync/lib-services-framework', () => ({
@@ -83,6 +83,7 @@ describe('Probe Routes', () => {
83
83
  });
84
84
 
85
85
  describe('livenessCheck', () => {
86
+ const mockedContext = { context: { service_context: { replicationEngine: {} } } } as any;
86
87
  it('has the correct route definitions', () => {
87
88
  expect(livenessCheck.path).toBe('/probes/liveness');
88
89
  expect(livenessCheck.method).toBe('GET');
@@ -97,7 +98,7 @@ describe('Probe Routes', () => {
97
98
 
98
99
  vi.mocked(container.probes.state).mockReturnValue(mockState);
99
100
 
100
- const response = await livenessCheck.handler();
101
+ const response = await livenessCheck.handler(mockedContext);
101
102
 
102
103
  expect(response.status).toBe(200);
103
104
  expect(response.data).toEqual(mockState);
@@ -112,7 +113,7 @@ describe('Probe Routes', () => {
112
113
 
113
114
  vi.mocked(container.probes.state).mockReturnValue(mockState);
114
115
 
115
- const response = await livenessCheck.handler();
116
+ const response = await livenessCheck.handler(mockedContext);
116
117
 
117
118
  expect(response.status).toBe(400);
118
119
  expect(response.data).toEqual(mockState);
@@ -8,7 +8,8 @@ import {
8
8
  SyncContext,
9
9
  WatchFilterEvent
10
10
  } from '@/index.js';
11
- import { RequestParameters, SqliteJsonRow, SqliteJsonValue, SqlSyncRules } from '@powersync/service-sync-rules';
11
+ import { JSONBig } from '@powersync/service-jsonbig';
12
+ import { RequestParameters, SqliteJsonRow, ParameterLookup, SqlSyncRules } from '@powersync/service-sync-rules';
12
13
  import { describe, expect, test } from 'vitest';
13
14
 
14
15
  describe('BucketChecksumState', () => {
@@ -97,9 +98,9 @@ bucket_definitions:
97
98
  base: { checkpoint: 2n, lsn: '2' },
98
99
  writeCheckpoint: null,
99
100
  update: {
100
- updatedDataBuckets: ['global[]'],
101
+ updatedDataBuckets: new Set(['global[]']),
101
102
  invalidateDataBuckets: false,
102
- updatedParameterBucketDefinitions: [],
103
+ updatedParameterLookups: new Set(),
103
104
  invalidateParameterBuckets: false
104
105
  }
105
106
  }))!;
@@ -200,7 +201,7 @@ bucket_definitions:
200
201
  writeCheckpoint: null,
201
202
  update: {
202
203
  ...CHECKPOINT_INVALIDATE_ALL,
203
- updatedDataBuckets: ['global[1]', 'global[2]'],
204
+ updatedDataBuckets: new Set(['global[1]', 'global[2]']),
204
205
  invalidateDataBuckets: false
205
206
  }
206
207
  }))!;
@@ -293,7 +294,7 @@ bucket_definitions:
293
294
  // Invalidate the state for global[1] - will only re-check the single bucket.
294
295
  // This is essentially inconsistent state, but is the simplest way to test that
295
296
  // the filter is working.
296
- updatedDataBuckets: ['global[1]'],
297
+ updatedDataBuckets: new Set(['global[1]']),
297
298
  invalidateDataBuckets: false
298
299
  }
299
300
  }))!;
@@ -420,7 +421,7 @@ bucket_definitions:
420
421
  update: {
421
422
  ...CHECKPOINT_INVALIDATE_ALL,
422
423
  invalidateDataBuckets: false,
423
- updatedDataBuckets: ['global[1]']
424
+ updatedDataBuckets: new Set(['global[1]'])
424
425
  }
425
426
  }))!;
426
427
  expect(line2.checkpointLine).toEqual({
@@ -474,10 +475,10 @@ bucket_definitions:
474
475
 
475
476
  storage.getParameterSets = async (
476
477
  checkpoint: InternalOpId,
477
- lookups: SqliteJsonValue[][]
478
+ lookups: ParameterLookup[]
478
479
  ): Promise<SqliteJsonRow[]> => {
479
480
  expect(checkpoint).toEqual(1n);
480
- expect(lookups).toEqual([['by_project', '1', 'u1']]);
481
+ expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
481
482
  return [{ id: 1 }, { id: 2 }];
482
483
  };
483
484
 
@@ -519,10 +520,10 @@ bucket_definitions:
519
520
 
520
521
  storage.getParameterSets = async (
521
522
  checkpoint: InternalOpId,
522
- lookups: SqliteJsonValue[][]
523
+ lookups: ParameterLookup[]
523
524
  ): Promise<SqliteJsonRow[]> => {
524
525
  expect(checkpoint).toEqual(2n);
525
- expect(lookups).toEqual([['by_project', '1', 'u1']]);
526
+ expect(lookups).toEqual([ParameterLookup.normalized('by_project', '1', ['u1'])]);
526
527
  return [{ id: 1 }, { id: 2 }, { id: 3 }];
527
528
  };
528
529
 
@@ -532,8 +533,8 @@ bucket_definitions:
532
533
  writeCheckpoint: null,
533
534
  update: {
534
535
  invalidateDataBuckets: false,
535
- updatedDataBuckets: [],
536
- updatedParameterBucketDefinitions: ['by_project'],
536
+ updatedDataBuckets: new Set(),
537
+ updatedParameterLookups: new Set([JSONBig.stringify(['by_project', '1', 'u1'])]),
537
538
  invalidateParameterBuckets: false
538
539
  }
539
540
  }))!;
@@ -580,7 +581,7 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
580
581
  );
581
582
  }
582
583
 
583
- async getParameterSets(checkpoint: InternalOpId, lookups: SqliteJsonValue[][]): Promise<SqliteJsonRow[]> {
584
+ async getParameterSets(checkpoint: InternalOpId, lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
584
585
  throw new Error('Method not implemented.');
585
586
  }
586
587
  }
@@ -0,0 +1,48 @@
1
+ import { getIntersection, hasIntersection } from '@/index.js';
2
+ import { describe, expect, test } from 'vitest';
3
+
4
+ describe('utils', () => {
5
+ function testInstersection(a: Set<any>, b: Set<any>, expected: boolean) {
6
+ expect(hasIntersection(a, b)).toBe(expected);
7
+ expect(hasIntersection(b, a)).toBe(expected);
8
+ const mapA = new Map([...a].map((v) => [v, 1]));
9
+ const mapB = new Map([...b].map((v) => [v, 1]));
10
+ expect(hasIntersection(mapA, b)).toBe(expected);
11
+ expect(hasIntersection(mapB, a)).toBe(expected);
12
+ expect(hasIntersection(mapA, mapB)).toBe(expected);
13
+ }
14
+
15
+ test('hasIntersection', async () => {
16
+ testInstersection(new Set(['a']), new Set(['a']), true);
17
+ testInstersection(new Set(['a', 'b', 'c']), new Set(['a', 'b', 'c']), true);
18
+ testInstersection(new Set(['a', 'b', 'c']), new Set(['d', 'e']), false);
19
+ testInstersection(new Set(['a', 'b', 'c']), new Set(['d', 'c', 'e']), true);
20
+ testInstersection(new Set(['a', 'b', 'c']), new Set(['c', 'e']), true);
21
+ testInstersection(new Set(['a', 'b', 'c', 2]), new Set([1, 2, 3]), true);
22
+ testInstersection(new Set(['a', 'b', 'c', 4]), new Set([1, 2, 3]), false);
23
+ testInstersection(new Set([]), new Set([1, 2, 3]), false);
24
+ testInstersection(new Set([]), new Set([]), false);
25
+ });
26
+
27
+ function testGetIntersection(a: Set<any>, b: Set<any>, expected: any[]) {
28
+ expect([...getIntersection(a, b)]).toEqual(expected);
29
+ expect([...getIntersection(b, a)]).toEqual(expected);
30
+ const mapA = new Map([...a].map((v) => [v, 1]));
31
+ const mapB = new Map([...b].map((v) => [v, 1]));
32
+ expect([...getIntersection(mapA, b)]).toEqual(expected);
33
+ expect([...getIntersection(mapB, a)]).toEqual(expected);
34
+ expect([...getIntersection(mapA, mapB)]).toEqual(expected);
35
+ }
36
+
37
+ test('getIntersection', async () => {
38
+ testGetIntersection(new Set(['a']), new Set(['a']), ['a']);
39
+ testGetIntersection(new Set(['a', 'b', 'c']), new Set(['a', 'b', 'c']), ['a', 'b', 'c']);
40
+ testGetIntersection(new Set(['a', 'b', 'c']), new Set(['d', 'e']), []);
41
+ testGetIntersection(new Set(['a', 'b', 'c']), new Set(['d', 'c', 'e']), ['c']);
42
+ testGetIntersection(new Set(['a', 'b', 'c']), new Set(['c', 'e']), ['c']);
43
+ testGetIntersection(new Set(['a', 'b', 'c', 2]), new Set([1, 2, 3]), [2]);
44
+ testGetIntersection(new Set(['a', 'b', 'c', 4]), new Set([1, 2, 3]), []);
45
+ testGetIntersection(new Set([]), new Set([1, 2, 3]), []);
46
+ testGetIntersection(new Set([]), new Set([]), []);
47
+ });
48
+ });