@luvio/adapter-test-library 0.105.0 → 0.108.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.
@@ -0,0 +1,53 @@
1
+ import type { DurableStorePersistence } from '../durableStorePersistence';
2
+ import { MockDurableStore } from '../MockDurableStore';
3
+
4
+ describe('MockDurableStore', () => {
5
+ describe('read/write synchronization', () => {
6
+ it('allows parallel reads', async () => {
7
+ const persistence: DurableStorePersistence = {
8
+ get: jest.fn().mockResolvedValue(undefined),
9
+ set: jest.fn().mockResolvedValue(undefined),
10
+ delete: jest.fn().mockResolvedValue(undefined),
11
+ };
12
+ const ds = new MockDurableStore(persistence);
13
+ const segment = 'foo';
14
+
15
+ // kick off reads without awaiting, all should start
16
+ const promise1 = ds.getAllEntries(segment);
17
+ const promise2 = ds.getAllEntries(segment);
18
+ const promise3 = ds.getEntries(['123'], segment);
19
+
20
+ expect(persistence.get).toBeCalledTimes(3);
21
+
22
+ const result1 = await promise1;
23
+ const result2 = await promise2;
24
+ const result3 = await promise3;
25
+
26
+ expect(result1).toBe(undefined);
27
+ expect(result2).toBe(undefined);
28
+ expect(result3).toBe(undefined);
29
+ });
30
+
31
+ it('read then write then read will respect ordering', async () => {
32
+ const ds = new MockDurableStore();
33
+ const segment = 'foo';
34
+ const key = '123';
35
+ const value = {};
36
+
37
+ const read1Promise = ds.getEntries([key], segment);
38
+ const write1Promise = ds.setEntries({ [key]: { data: value } }, segment);
39
+ const read2Promise = ds.getEntries([key], segment);
40
+
41
+ // first read should be empty
42
+ const read1Result = await read1Promise;
43
+ expect(read1Result).toBe(undefined);
44
+
45
+ // wait for the write to finish
46
+ await write1Promise;
47
+
48
+ // second read should include data from the write
49
+ const read2Result = await read2Promise;
50
+ expect(read2Result).toEqual({ [key]: { data: value } });
51
+ });
52
+ });
53
+ });
@@ -1,7 +1,10 @@
1
+ import { flushPromises } from './utils';
2
+
1
3
  export interface DurableStorePersistence {
2
4
  get<T>(key: string): Promise<T | undefined>;
3
5
  set<T>(key: string, value: T): Promise<void>;
4
6
  delete(key: string): Promise<void>;
7
+ flushPendingWork(): Promise<void>;
5
8
  }
6
9
 
7
10
  export class MemoryDurableStorePersistence implements DurableStorePersistence {
@@ -12,10 +15,20 @@ export class MemoryDurableStorePersistence implements DurableStorePersistence {
12
15
  }
13
16
 
14
17
  async set<T>(key: string, value: T): Promise<void> {
18
+ // simulate a more realistic durable store by making the write wait a
19
+ // tick before actually setting the value
20
+ await flushPromises();
15
21
  this.store[key] = value;
16
22
  }
17
23
 
18
24
  async delete(key: string): Promise<void> {
19
25
  delete this.store[key];
20
26
  }
27
+
28
+ async flushPendingWork(): Promise<void> {
29
+ // since this implementation does actual "IO" synchronously the only
30
+ // thing necessary to await any pending IO is to flush the current
31
+ // microtask queue
32
+ await flushPromises();
33
+ }
21
34
  }
package/src/main.ts CHANGED
@@ -10,9 +10,10 @@ export {
10
10
  setNetworkConnectivity,
11
11
  buildFetchResponse,
12
12
  overrideMockNetworkResponses,
13
+ flushPendingNetworkRequests,
13
14
  } from './network';
14
15
  export { verifyImmutable, isImmutable } from './verification';
15
16
  export { getMockLuvioWithFulfilledSnapshot, getMockFulfilledSnapshot } from './mocks';
16
17
  export { stripProperties } from './utils';
17
- export { MockDurableStore } from './durableStore';
18
+ export { MockDurableStore } from './MockDurableStore';
18
19
  export { MemoryDurableStorePersistence, DurableStorePersistence } from './durableStorePersistence';
package/src/network.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { NetworkAdapter, ResourceRequest, FetchResponse, Headers } from '@luvio/engine';
2
2
  import sinon from 'sinon';
3
3
 
4
- import { clone } from './utils';
4
+ import { clone, flushPromises } from './utils';
5
5
 
6
6
  const networkConnectivityStateMap = new WeakMap<sinon.SinonStub, ConnectivityState>();
7
7
 
@@ -68,6 +68,21 @@ function isOkResponse(status: number) {
68
68
  return status >= 200 && status <= 299;
69
69
  }
70
70
 
71
+ /**
72
+ * Flushes any pending network requests. Useful for tests that need to ensure all
73
+ * un-awaited background refreshes are complete
74
+ *
75
+ * @param _mockNetworkAdapter {NetworkAdapter} The network adapter instance to flush
76
+ */
77
+ export async function flushPendingNetworkRequests(
78
+ _mockNetworkAdapter: NetworkAdapter
79
+ ): Promise<void> {
80
+ // since the network mock is actually synchronous (just returns things wrapped
81
+ // in Promise.resolve/reject) the only thing necessary to flush any pending
82
+ // network activity is to flush the pending microtask queue
83
+ await flushPromises();
84
+ }
85
+
71
86
  export function buildMockNetworkAdapter(mockPayloads: MockPayload[]) {
72
87
  // any endpoints not setup with a fake will return a rejected promise
73
88
  const networkAdapter = sinon.stub().rejects(buildMockSetupError());
package/src/utils.ts CHANGED
@@ -51,3 +51,7 @@ export function doesThrow(predicate: () => void) {
51
51
  }
52
52
  return false;
53
53
  }
54
+
55
+ export function flushPromises() {
56
+ return new Promise((resolve) => setTimeout(resolve, 0));
57
+ }
@@ -1,108 +0,0 @@
1
- import type {
2
- DurableStore,
3
- DurableStoreEntries,
4
- OnDurableStoreChangedListener,
5
- DurableStoreOperation,
6
- DurableStoreChange,
7
- } from '@luvio/environments';
8
- import { DurableStoreOperationType } from '@luvio/environments';
9
- import { clone } from './utils';
10
- import type { DurableStorePersistence } from './durableStorePersistence';
11
- import { MemoryDurableStorePersistence } from './durableStorePersistence';
12
- export class MockDurableStore implements DurableStore {
13
- // NOTE: This mock class doesn't enforce read/write synchronization
14
-
15
- listeners = new Set<OnDurableStoreChangedListener>();
16
- persistence: DurableStorePersistence;
17
-
18
- constructor(persistence?: DurableStorePersistence) {
19
- this.persistence = persistence || new MemoryDurableStorePersistence();
20
- }
21
-
22
- getEntries<T>(
23
- entryIds: string[],
24
- segment: string
25
- ): Promise<DurableStoreEntries<T> | undefined> {
26
- const returnSource = Object.create(null);
27
- return this.persistence.get<DurableStoreEntries<T>>(segment).then((entries) => {
28
- if (entries === undefined) {
29
- return undefined;
30
- }
31
-
32
- for (const entryId of entryIds) {
33
- const entry = entries[entryId];
34
- if (entry !== undefined) {
35
- returnSource[entryId] = clone(entry);
36
- }
37
- }
38
- return returnSource;
39
- });
40
- }
41
-
42
- getAllEntries<T>(segment: string): Promise<DurableStoreEntries<T>> {
43
- const returnSource = Object.create(null);
44
-
45
- return this.persistence.get<DurableStoreEntries<T>>(segment).then((rawEntries) => {
46
- const entries = rawEntries === undefined ? {} : rawEntries;
47
-
48
- for (const key of Object.keys(entries)) {
49
- returnSource[key] = clone(entries[key]);
50
- }
51
- return returnSource;
52
- });
53
- }
54
-
55
- setEntries<T>(entries: DurableStoreEntries<T>, segment: string): Promise<void> {
56
- return this.batchOperations([
57
- { entries, segment, type: DurableStoreOperationType.SetEntries },
58
- ]);
59
- }
60
-
61
- evictEntries(ids: string[], segment: string): Promise<void> {
62
- return this.batchOperations([
63
- { ids, segment, type: DurableStoreOperationType.EvictEntries },
64
- ]);
65
- }
66
-
67
- registerOnChangedListener(listener: OnDurableStoreChangedListener): () => Promise<void> {
68
- this.listeners.add(listener);
69
- return () => {
70
- this.listeners.delete(listener);
71
- return Promise.resolve();
72
- };
73
- }
74
-
75
- async batchOperations<T>(operations: DurableStoreOperation<T>[]): Promise<void> {
76
- const changes: DurableStoreChange[] = [];
77
- for (let i = 0; i < operations.length; i++) {
78
- changes.push(await this.performOperation(operations[i]));
79
- }
80
-
81
- this.listeners.forEach((listener) => {
82
- listener(changes);
83
- });
84
- }
85
-
86
- async performOperation<T>(operation: DurableStoreOperation<T>): Promise<DurableStoreChange> {
87
- const segment = operation.segment;
88
- const rawEntries = await this.persistence.get<DurableStoreEntries<T>>(segment);
89
- const entries = rawEntries === undefined ? {} : rawEntries;
90
- let ids: string[] = [];
91
- switch (operation.type) {
92
- case DurableStoreOperationType.SetEntries:
93
- ids = Object.keys(operation.entries);
94
- ids.forEach((id) => {
95
- entries[id] = clone(operation.entries[id]);
96
- });
97
- break;
98
- case DurableStoreOperationType.EvictEntries:
99
- ids = operation.ids;
100
- ids.forEach((id) => {
101
- delete entries[id];
102
- });
103
- }
104
-
105
- await this.persistence.set(operation.segment, entries);
106
- return { ids, segment, type: operation.type };
107
- }
108
- }