Skip to content

Commit 9f90a07

Browse files
authored
Firestore: testing_hooks.ts: move some logic into testing_hooks_spi.ts (#7543)
1 parent 4ae8041 commit 9f90a07

File tree

6 files changed

+183
-149
lines changed

6 files changed

+183
-149
lines changed

packages/firestore/externs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"packages/firestore/src/util/error.ts",
3737
"packages/firestore/src/local/indexeddb_schema.ts",
3838
"packages/firestore/src/local/indexeddb_schema_legacy.ts",
39-
"packages/firestore/src/local/shared_client_state_schema.ts"
39+
"packages/firestore/src/local/shared_client_state_schema.ts",
40+
"packages/firestore/src/util/testing_hooks.ts"
4041
]
4142
}

packages/firestore/src/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,8 @@ export type {
223223
} from './api/credentials';
224224
export { EmptyAuthCredentialsProvider as _EmptyAuthCredentialsProvider } from './api/credentials';
225225
export { EmptyAppCheckTokenProvider as _EmptyAppCheckTokenProvider } from './api/credentials';
226-
export { TestingHooks as _TestingHooks } from './util/testing_hooks';
226+
export {
227+
ExistenceFilterMismatchCallback as _TestingHooksExistenceFilterMismatchCallback,
228+
TestingHooks as _TestingHooks
229+
} from './util/testing_hooks';
230+
export { ExistenceFilterMismatchInfo as _TestingHooksExistenceFilterMismatchInfo } from './util/testing_hooks_spi';

packages/firestore/src/remote/watch_change.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ import { primitiveComparator } from '../util/misc';
3838
import { SortedMap } from '../util/sorted_map';
3939
import { SortedSet } from '../util/sorted_set';
4040
import {
41-
ExistenceFilterMismatchInfo as TestingHooksExistenceFilterMismatchInfo,
42-
TestingHooks
43-
} from '../util/testing_hooks';
41+
testingHooksSpi,
42+
ExistenceFilterMismatchInfo as TestingHooksExistenceFilterMismatchInfo
43+
} from '../util/testing_hooks_spi';
4444

4545
import { BloomFilter, BloomFilterError } from './bloom_filter';
4646
import { ExistenceFilter } from './existence_filter';
@@ -452,7 +452,7 @@ export class WatchChangeAggregator {
452452
purpose
453453
);
454454
}
455-
TestingHooks.instance?.notifyOnExistenceFilterMismatch(
455+
testingHooksSpi?.notifyOnExistenceFilterMismatch(
456456
createExistenceFilterMismatchInfoForTestingHooks(
457457
currentSize,
458458
watchChange.existenceFilter,

packages/firestore/src/util/testing_hooks.ts

Lines changed: 55 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,24 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { Unsubscribe } from '../api/reference_impl';
19+
20+
import {
21+
setTestingHooksSpi,
22+
ExistenceFilterMismatchInfo,
23+
TestingHooksSpi
24+
} from './testing_hooks_spi';
25+
1826
/**
19-
* Manages "testing hooks", hooks into the internals of the SDK to verify
20-
* internal state and events during integration tests. Do not use this class
21-
* except for testing purposes.
22-
*
23-
* There are two ways to retrieve the global singleton instance of this class:
24-
* 1. The `instance` property, which returns null if the global singleton
25-
* instance has not been created. Use this property if the caller should
26-
* "do nothing" if there are no testing hooks registered, such as when
27-
* delivering an event to notify registered callbacks.
28-
* 2. The `getOrCreateInstance()` method, which creates the global singleton
29-
* instance if it has not been created. Use this method if the instance is
30-
* needed to, for example, register a callback.
27+
* Testing hooks for use by Firestore's integration test suite to reach into the
28+
* SDK internals to validate logic and behavior that is not visible from the
29+
* public API surface.
3130
*
3231
* @internal
3332
*/
3433
export class TestingHooks {
35-
private readonly onExistenceFilterMismatchCallbacks = new Map<
36-
Symbol,
37-
ExistenceFilterMismatchCallback
38-
>();
39-
40-
private constructor() {}
41-
42-
/**
43-
* Returns the singleton instance of this class, or null if it has not been
44-
* initialized.
45-
*/
46-
static get instance(): TestingHooks | null {
47-
return gTestingHooksSingletonInstance;
48-
}
49-
50-
/**
51-
* Returns the singleton instance of this class, creating it if is has never
52-
* been created before.
53-
*/
54-
static getOrCreateInstance(): TestingHooks {
55-
if (gTestingHooksSingletonInstance === null) {
56-
gTestingHooksSingletonInstance = new TestingHooks();
57-
}
58-
return gTestingHooksSingletonInstance;
34+
private constructor() {
35+
throw new Error('instances of this class should not be created');
5936
}
6037

6138
/**
@@ -72,87 +49,58 @@ export class TestingHooks {
7249
* the first invocation of the returned function does anything; all subsequent
7350
* invocations do nothing.
7451
*/
75-
onExistenceFilterMismatch(
52+
static onExistenceFilterMismatch(
7653
callback: ExistenceFilterMismatchCallback
77-
): () => void {
78-
const key = Symbol();
79-
this.onExistenceFilterMismatchCallbacks.set(key, callback);
80-
return () => this.onExistenceFilterMismatchCallbacks.delete(key);
81-
}
82-
83-
/**
84-
* Invokes all currently-registered `onExistenceFilterMismatch` callbacks.
85-
* @param info Information about the existence filter mismatch.
86-
*/
87-
notifyOnExistenceFilterMismatch(info: ExistenceFilterMismatchInfo): void {
88-
this.onExistenceFilterMismatchCallbacks.forEach(callback => callback(info));
54+
): Unsubscribe {
55+
return TestingHooksSpiImpl.instance.onExistenceFilterMismatch(callback);
8956
}
9057
}
9158

9259
/**
93-
* Information about an existence filter mismatch, as specified to callbacks
94-
* registered with `TestingUtils.onExistenceFilterMismatch()`.
60+
* The signature of callbacks registered with
61+
* `TestingUtils.onExistenceFilterMismatch()`.
62+
*
63+
* The return value, if any, is ignored.
64+
*
65+
* @internal
9566
*/
96-
export interface ExistenceFilterMismatchInfo {
97-
/** The number of documents that matched the query in the local cache. */
98-
localCacheCount: number;
99-
100-
/**
101-
* The number of documents that matched the query on the server, as specified
102-
* in the ExistenceFilter message's `count` field.
103-
*/
104-
existenceFilterCount: number;
105-
106-
/**
107-
* The projectId used when checking documents for membership in the bloom
108-
* filter.
109-
*/
110-
projectId: string;
111-
112-
/**
113-
* The databaseId used when checking documents for membership in the bloom
114-
* filter.
115-
*/
116-
databaseId: string;
67+
export type ExistenceFilterMismatchCallback = (
68+
info: ExistenceFilterMismatchInfo
69+
) => unknown;
11770

118-
/**
119-
* Information about the bloom filter provided by Watch in the ExistenceFilter
120-
* message's `unchangedNames` field. If this property is omitted or undefined
121-
* then that means that Watch did _not_ provide a bloom filter.
122-
*/
123-
bloomFilter?: {
124-
/**
125-
* Whether a full requery was averted by using the bloom filter. If false,
126-
* then something happened, such as a false positive, to prevent using the
127-
* bloom filter to avoid a full requery.
128-
*/
129-
applied: boolean;
71+
/**
72+
* The implementation of `TestingHooksSpi`.
73+
*/
74+
class TestingHooksSpiImpl implements TestingHooksSpi {
75+
private readonly existenceFilterMismatchCallbacksById = new Map<
76+
Symbol,
77+
ExistenceFilterMismatchCallback
78+
>();
13079

131-
/** The number of hash functions used in the bloom filter. */
132-
hashCount: number;
80+
private constructor() {}
13381

134-
/** The number of bytes in the bloom filter's bitmask. */
135-
bitmapLength: number;
82+
static get instance(): TestingHooksSpiImpl {
83+
if (!testingHooksSpiImplInstance) {
84+
testingHooksSpiImplInstance = new TestingHooksSpiImpl();
85+
setTestingHooksSpi(testingHooksSpiImplInstance);
86+
}
87+
return testingHooksSpiImplInstance;
88+
}
13689

137-
/** The number of bits of padding in the last byte of the bloom filter. */
138-
padding: number;
90+
notifyOnExistenceFilterMismatch(info: ExistenceFilterMismatchInfo): void {
91+
this.existenceFilterMismatchCallbacksById.forEach(callback =>
92+
callback(info)
93+
);
94+
}
13995

140-
/**
141-
* Tests the given string for membership in the bloom filter created from
142-
* the existence filter; will be undefined if creating the bloom filter
143-
* failed.
144-
*/
145-
mightContain?: (value: string) => boolean;
146-
};
96+
onExistenceFilterMismatch(
97+
callback: ExistenceFilterMismatchCallback
98+
): Unsubscribe {
99+
const id = Symbol();
100+
const callbacks = this.existenceFilterMismatchCallbacksById;
101+
callbacks.set(id, callback);
102+
return () => callbacks.delete(id);
103+
}
147104
}
148105

149-
/**
150-
* The signature of callbacks registered with
151-
* `TestingUtils.onExistenceFilterMismatch()`.
152-
*/
153-
export type ExistenceFilterMismatchCallback = (
154-
info: ExistenceFilterMismatchInfo
155-
) => void;
156-
157-
/** The global singleton instance of `TestingHooks`. */
158-
let gTestingHooksSingletonInstance: TestingHooks | null = null;
106+
let testingHooksSpiImplInstance: TestingHooksSpiImpl | null = null;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* The global, singleton instance of TestingHooksSpi.
20+
*
21+
* This variable will be `null` in all cases _except_ when running from
22+
* integration tests that have registered callbacks to be notified of events
23+
* that happen during the test execution.
24+
*/
25+
export let testingHooksSpi: TestingHooksSpi | null = null;
26+
27+
/**
28+
* Sets the value of the `testingHooksSpi` object.
29+
* @param instance the instance to set.
30+
*/
31+
export function setTestingHooksSpi(instance: TestingHooksSpi): void {
32+
if (testingHooksSpi) {
33+
throw new Error('a TestingHooksSpi instance is already set');
34+
}
35+
testingHooksSpi = instance;
36+
}
37+
38+
/**
39+
* The "service provider interface" for the testing hooks.
40+
*
41+
* The implementation of this object will handle the callbacks made by the SDK
42+
* to be handled by the integration tests.
43+
*
44+
* This "SPI" is separated from the implementation to avoid import cycles and
45+
* to enable production builds to fully tree-shake away the testing hooks logic.
46+
*/
47+
export interface TestingHooksSpi {
48+
/**
49+
* Invokes all callbacks registered with
50+
* `TestingHooks.onExistenceFilterMismatch()` with the given info.
51+
*/
52+
notifyOnExistenceFilterMismatch(info: ExistenceFilterMismatchInfo): void;
53+
}
54+
55+
/**
56+
* Information about an existence filter mismatch.
57+
* @internal
58+
*/
59+
export interface ExistenceFilterMismatchInfo {
60+
/** The number of documents that matched the query in the local cache. */
61+
localCacheCount: number;
62+
63+
/**
64+
* The number of documents that matched the query on the server, as specified
65+
* in the ExistenceFilter message's `count` field.
66+
*/
67+
existenceFilterCount: number;
68+
69+
/**
70+
* The projectId used when checking documents for membership in the bloom
71+
* filter.
72+
*/
73+
projectId: string;
74+
75+
/**
76+
* The databaseId used when checking documents for membership in the bloom
77+
* filter.
78+
*/
79+
databaseId: string;
80+
81+
/**
82+
* Information about the bloom filter provided by Watch in the ExistenceFilter
83+
* message's `unchangedNames` field. If this property is omitted or undefined
84+
* then that means that Watch did _not_ provide a bloom filter.
85+
*/
86+
bloomFilter?: {
87+
/**
88+
* Whether a full requery was averted by using the bloom filter. If false,
89+
* then something happened, such as a false positive, to prevent using the
90+
* bloom filter to avoid a full requery.
91+
*/
92+
applied: boolean;
93+
94+
/** The number of hash functions used in the bloom filter. */
95+
hashCount: number;
96+
97+
/** The number of bytes in the bloom filter's bitmask. */
98+
bitmapLength: number;
99+
100+
/** The number of bits of padding in the last byte of the bloom filter. */
101+
padding: number;
102+
103+
/**
104+
* Tests the given string for membership in the bloom filter created from
105+
* the existence filter; will be undefined if creating the bloom filter
106+
* failed.
107+
*/
108+
mightContain?: (value: string) => boolean;
109+
};
110+
}

0 commit comments

Comments
 (0)