@powersync/service-core 0.14.0 → 0.16.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/CHANGELOG.md +33 -0
- package/dist/auth/RemoteJWKSCollector.d.ts +9 -7
- package/dist/auth/RemoteJWKSCollector.js +16 -29
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/entry/cli-entry.js +2 -0
- package/dist/entry/cli-entry.js.map +1 -1
- package/dist/entry/commands/teardown-action.js +2 -1
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/entry/commands/test-connection-action.d.ts +2 -0
- package/dist/entry/commands/test-connection-action.js +32 -0
- package/dist/entry/commands/test-connection-action.js.map +1 -0
- package/dist/metrics/Metrics.js +2 -2
- package/dist/metrics/Metrics.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +2 -0
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationEngine.d.ts +2 -0
- package/dist/replication/ReplicationEngine.js +3 -0
- package/dist/replication/ReplicationEngine.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +8 -2
- package/dist/replication/ReplicationModule.js +6 -11
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/endpoints/admin.js +3 -3
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +5 -5
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +9 -9
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +5 -5
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.js +4 -4
- package/dist/routes/route-register.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +2 -2
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +14 -4
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +0 -3
- package/dist/util/utils.d.ts +4 -0
- package/dist/util/utils.js +31 -1
- package/dist/util/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/auth/RemoteJWKSCollector.ts +31 -35
- package/src/entry/cli-entry.ts +2 -0
- package/src/entry/commands/teardown-action.ts +2 -1
- package/src/entry/commands/test-connection-action.ts +41 -0
- package/src/metrics/Metrics.ts +2 -2
- package/src/replication/AbstractReplicator.ts +3 -0
- package/src/replication/ReplicationEngine.ts +5 -0
- package/src/replication/ReplicationModule.ts +15 -13
- package/src/routes/endpoints/admin.ts +3 -3
- package/src/routes/endpoints/socket-route.ts +5 -5
- package/src/routes/endpoints/sync-rules.ts +9 -9
- package/src/routes/endpoints/sync-stream.ts +5 -5
- package/src/routes/route-register.ts +4 -4
- package/src/storage/BucketStorage.ts +6 -2
- package/src/util/config/compound-config-collector.ts +17 -9
- package/src/util/config/types.ts +0 -3
- package/src/util/utils.ts +28 -1
- package/test/src/auth.test.ts +20 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { DataSourceConfig } from '@powersync/service-types/dist/config/PowerSyncConfig.js';
|
|
2
2
|
import * as t from 'ts-codec';
|
|
3
3
|
|
|
4
|
+
import { schema } from '@powersync/lib-services-framework';
|
|
4
5
|
import * as types from '@powersync/service-types';
|
|
5
6
|
import * as api from '../api/api-index.js';
|
|
6
7
|
import * as modules from '../modules/modules-index.js';
|
|
7
8
|
import * as system from '../system/system-index.js';
|
|
8
|
-
import { schema } from '@powersync/lib-services-framework';
|
|
9
9
|
import { AbstractReplicator } from './AbstractReplicator.js';
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
export interface ConnectionTestResult {
|
|
12
|
+
/**
|
|
13
|
+
* Connection URI or hostname.
|
|
14
|
+
*/
|
|
15
|
+
connectionDescription: string;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Provides a common interface for testing the connection to a DataSource.
|
|
@@ -17,7 +23,7 @@ export interface ConnectionTester<TConfig extends DataSourceConfig> {
|
|
|
17
23
|
* Confirm if a connection can be established to the datasource for the provided datasource configuration
|
|
18
24
|
* @param config
|
|
19
25
|
*/
|
|
20
|
-
testConnection(config: TConfig): Promise<
|
|
26
|
+
testConnection(config: TConfig): Promise<ConnectionTestResult>;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface ReplicationModuleOptions extends modules.AbstractModuleOptions {
|
|
@@ -58,7 +64,7 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
|
|
|
58
64
|
*/
|
|
59
65
|
protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
|
|
60
66
|
|
|
61
|
-
public abstract testConnection(config: TConfig): Promise<
|
|
67
|
+
public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* Register this module's Replicators and RouteAPI adapters if the required configuration is present.
|
|
@@ -81,16 +87,12 @@ export abstract class ReplicationModule<TConfig extends DataSourceConfig>
|
|
|
81
87
|
);
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.decodeConfig(baseMatchingConfig);
|
|
90
|
+
const baseMatchingConfig = matchingConfig[0] as TConfig;
|
|
91
|
+
// If decoding fails, this will raise a hard error, and stop the service.
|
|
92
|
+
this.decodeConfig(baseMatchingConfig);
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} catch (e) {
|
|
92
|
-
this.logger.error('Failed to initialize.', e);
|
|
93
|
-
}
|
|
94
|
+
context.replicationEngine?.register(this.createReplicator(context));
|
|
95
|
+
context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
protected decodeConfig(config: TConfig): void {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errors, router, schema } from '@powersync/lib-services-framework';
|
|
1
|
+
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
2
2
|
import { SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
|
|
3
3
|
import { internal_routes } from '@powersync/service-types';
|
|
4
4
|
|
|
@@ -120,9 +120,9 @@ export const reprocess = routeDefinition({
|
|
|
120
120
|
|
|
121
121
|
const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
122
122
|
if (active == null) {
|
|
123
|
-
throw new errors.
|
|
123
|
+
throw new errors.ServiceError({
|
|
124
124
|
status: 422,
|
|
125
|
-
code:
|
|
125
|
+
code: ErrorCode.PSYNC_S4104,
|
|
126
126
|
description: 'No active sync rules'
|
|
127
127
|
});
|
|
128
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errors, logger, schema } from '@powersync/lib-services-framework';
|
|
1
|
+
import { ErrorCode, errors, logger, schema } from '@powersync/lib-services-framework';
|
|
2
2
|
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
3
|
import { serialize } from 'bson';
|
|
4
4
|
|
|
@@ -34,9 +34,9 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
34
34
|
|
|
35
35
|
if (routerEngine!.closed) {
|
|
36
36
|
responder.onError(
|
|
37
|
-
new errors.
|
|
37
|
+
new errors.ServiceError({
|
|
38
38
|
status: 503,
|
|
39
|
-
code:
|
|
39
|
+
code: ErrorCode.PSYNC_S2003,
|
|
40
40
|
description: 'Service temporarily unavailable'
|
|
41
41
|
})
|
|
42
42
|
);
|
|
@@ -53,9 +53,9 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
53
53
|
const cp = await activeBucketStorage.getActiveCheckpoint();
|
|
54
54
|
if (!cp.hasSyncRules()) {
|
|
55
55
|
responder.onError(
|
|
56
|
-
new errors.
|
|
56
|
+
new errors.ServiceError({
|
|
57
57
|
status: 500,
|
|
58
|
-
code:
|
|
58
|
+
code: ErrorCode.PSYNC_S2302,
|
|
59
59
|
description: 'No sync rules available'
|
|
60
60
|
})
|
|
61
61
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errors, router, schema } from '@powersync/lib-services-framework';
|
|
1
|
+
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
2
2
|
import { SqlSyncRules, SyncRulesErrors } from '@powersync/service-sync-rules';
|
|
3
3
|
import type { FastifyPluginAsync } from 'fastify';
|
|
4
4
|
import * as t from 'ts-codec';
|
|
@@ -43,9 +43,9 @@ export const deploySyncRules = routeDefinition({
|
|
|
43
43
|
|
|
44
44
|
if (service_context.configuration.sync_rules.present) {
|
|
45
45
|
// If sync rules are configured via the config, disable deploy via the API.
|
|
46
|
-
throw new errors.
|
|
46
|
+
throw new errors.ServiceError({
|
|
47
47
|
status: 422,
|
|
48
|
-
code:
|
|
48
|
+
code: ErrorCode.PSYNC_S4105,
|
|
49
49
|
description: 'Sync rules API disabled',
|
|
50
50
|
details: 'Use the management API to deploy sync rules'
|
|
51
51
|
});
|
|
@@ -60,9 +60,9 @@ export const deploySyncRules = routeDefinition({
|
|
|
60
60
|
schema: undefined
|
|
61
61
|
});
|
|
62
62
|
} catch (e) {
|
|
63
|
-
throw new errors.
|
|
63
|
+
throw new errors.ServiceError({
|
|
64
64
|
status: 422,
|
|
65
|
-
code:
|
|
65
|
+
code: ErrorCode.PSYNC_R0001,
|
|
66
66
|
description: 'Sync rules parsing failed',
|
|
67
67
|
details: e.message
|
|
68
68
|
});
|
|
@@ -112,9 +112,9 @@ export const currentSyncRules = routeDefinition({
|
|
|
112
112
|
|
|
113
113
|
const sync_rules = await activeBucketStorage.getActiveSyncRulesContent();
|
|
114
114
|
if (!sync_rules) {
|
|
115
|
-
throw new errors.
|
|
115
|
+
throw new errors.ServiceError({
|
|
116
116
|
status: 422,
|
|
117
|
-
code:
|
|
117
|
+
code: ErrorCode.PSYNC_S4104,
|
|
118
118
|
description: 'No active sync rules'
|
|
119
119
|
});
|
|
120
120
|
}
|
|
@@ -159,9 +159,9 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
159
159
|
const apiHandler = payload.context.service_context.routerEngine!.getAPI();
|
|
160
160
|
const sync_rules = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
161
161
|
if (sync_rules == null) {
|
|
162
|
-
throw new errors.
|
|
162
|
+
throw new errors.ServiceError({
|
|
163
163
|
status: 422,
|
|
164
|
-
code:
|
|
164
|
+
code: ErrorCode.PSYNC_S4104,
|
|
165
165
|
description: 'No active sync rules'
|
|
166
166
|
});
|
|
167
167
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errors, logger, router, schema } from '@powersync/lib-services-framework';
|
|
1
|
+
import { ErrorCode, errors, logger, router, schema } from '@powersync/lib-services-framework';
|
|
2
2
|
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
4
|
|
|
@@ -26,9 +26,9 @@ export const syncStreamed = routeDefinition({
|
|
|
26
26
|
const clientId = payload.params.client_id;
|
|
27
27
|
|
|
28
28
|
if (routerEngine!.closed) {
|
|
29
|
-
throw new errors.
|
|
29
|
+
throw new errors.ServiceError({
|
|
30
30
|
status: 503,
|
|
31
|
-
code:
|
|
31
|
+
code: ErrorCode.PSYNC_S2003,
|
|
32
32
|
description: 'Service temporarily unavailable'
|
|
33
33
|
});
|
|
34
34
|
}
|
|
@@ -39,9 +39,9 @@ export const syncStreamed = routeDefinition({
|
|
|
39
39
|
// Sanity check before we start the stream
|
|
40
40
|
const cp = await storageEngine.activeBucketStorage.getActiveCheckpoint();
|
|
41
41
|
if (!cp.hasSyncRules()) {
|
|
42
|
-
throw new errors.
|
|
42
|
+
throw new errors.ServiceError({
|
|
43
43
|
status: 500,
|
|
44
|
-
code:
|
|
44
|
+
code: ErrorCode.PSYNC_S2302,
|
|
45
45
|
description: 'No sync rules available'
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -62,16 +62,16 @@ export function registerFastifyRoutes(
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
} catch (ex) {
|
|
65
|
-
const
|
|
66
|
-
logger.error(`Request failed`,
|
|
65
|
+
const serviceError = errors.asServiceError(ex);
|
|
66
|
+
logger.error(`Request failed`, serviceError);
|
|
67
67
|
|
|
68
68
|
response = new router.RouterResponse({
|
|
69
|
-
status:
|
|
69
|
+
status: serviceError.errorData.status || 500,
|
|
70
70
|
headers: {
|
|
71
71
|
'Content-Type': 'application/json'
|
|
72
72
|
},
|
|
73
73
|
data: {
|
|
74
|
-
error:
|
|
74
|
+
error: serviceError.errorData
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AsyncDisposableObserverClient,
|
|
3
|
+
DisposableListener,
|
|
4
|
+
DisposableObserverClient
|
|
5
|
+
} from '@powersync/lib-services-framework';
|
|
2
6
|
import {
|
|
3
7
|
EvaluatedParameters,
|
|
4
8
|
EvaluatedRow,
|
|
@@ -56,7 +60,7 @@ export interface BucketStorageFactoryListener extends DisposableListener {
|
|
|
56
60
|
replicationEvent: (event: ReplicationEventPayload) => void;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export interface BucketStorageFactory extends
|
|
63
|
+
export interface BucketStorageFactory extends AsyncDisposableObserverClient<BucketStorageFactoryListener> {
|
|
60
64
|
/**
|
|
61
65
|
* Update sync rules from configuration, if changed.
|
|
62
66
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
1
|
+
import { logger, LookupOptions } from '@powersync/lib-services-framework';
|
|
2
2
|
import { configFile } from '@powersync/service-types';
|
|
3
3
|
import * as auth from '../../auth/auth-index.js';
|
|
4
4
|
import { ConfigCollector } from './collectors/config-collector.js';
|
|
@@ -91,12 +91,23 @@ export class CompoundConfigCollector {
|
|
|
91
91
|
jwks_uris = [jwks_uris];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
let jwksLookup: LookupOptions = {
|
|
95
|
+
reject_ip_ranges: []
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (baseConfig.client_auth?.jwks_reject_ip_ranges != null) {
|
|
99
|
+
jwksLookup = {
|
|
100
|
+
reject_ip_ranges: baseConfig.client_auth?.jwks_reject_ip_ranges
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (baseConfig.client_auth?.block_local_jwks) {
|
|
104
|
+
// Deprecated - recommend method is to use jwks_reject_ip_ranges
|
|
105
|
+
jwksLookup.reject_ip_ranges.push('local');
|
|
106
|
+
jwksLookup.reject_ipv6 = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
for (let uri of jwks_uris) {
|
|
95
|
-
collectors.add(
|
|
96
|
-
new auth.CachedKeyCollector(
|
|
97
|
-
new auth.RemoteJWKSCollector(uri, { block_local_ip: !!baseConfig.client_auth?.block_local_jwks })
|
|
98
|
-
)
|
|
99
|
-
);
|
|
110
|
+
collectors.add(new auth.CachedKeyCollector(new auth.RemoteJWKSCollector(uri, { lookupOptions: jwksLookup })));
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
const baseDevKey = (baseConfig.client_auth?.jwks?.keys ?? []).find((key) => key.kid == POWERSYNC_DEV_KID);
|
|
@@ -121,9 +132,6 @@ export class CompoundConfigCollector {
|
|
|
121
132
|
api_tokens: baseConfig.api?.tokens ?? [],
|
|
122
133
|
dev: {
|
|
123
134
|
demo_auth: baseConfig.dev?.demo_auth ?? false,
|
|
124
|
-
demo_client: baseConfig.dev?.demo_client ?? false,
|
|
125
|
-
demo_password: baseConfig.dev?.demo_password,
|
|
126
|
-
crud_api: baseConfig.dev?.crud_api ?? false,
|
|
127
135
|
dev_key: devKey
|
|
128
136
|
},
|
|
129
137
|
port: baseConfig.port ?? 8080,
|
package/src/util/config/types.ts
CHANGED
package/src/util/utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { BucketChecksum, OpId, OplogEntry } from './protocol-types.js';
|
|
|
7
7
|
import * as storage from '../storage/storage-index.js';
|
|
8
8
|
|
|
9
9
|
import { PartialChecksum } from '../storage/ChecksumCache.js';
|
|
10
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
10
11
|
|
|
11
12
|
export type ChecksumMap = Map<string, BucketChecksum>;
|
|
12
13
|
|
|
@@ -34,7 +35,7 @@ export function timestampToOpId(ts: bigint): OpId {
|
|
|
34
35
|
// Dynamic values are passed in in some cases, so we make extra sure that the
|
|
35
36
|
// number is a bigint and not number or Long.
|
|
36
37
|
if (typeof ts != 'bigint') {
|
|
37
|
-
throw new
|
|
38
|
+
throw new ServiceAssertionError(`bigint expected, got: ${ts} (${typeof ts})`);
|
|
38
39
|
}
|
|
39
40
|
return ts.toString(10);
|
|
40
41
|
}
|
|
@@ -222,3 +223,29 @@ export function flatstr(s: string) {
|
|
|
222
223
|
function rowKey(entry: OplogEntry) {
|
|
223
224
|
return `${entry.object_type}/${entry.object_id}/${entry.subkey}`;
|
|
224
225
|
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Estimate in-memory size of row.
|
|
229
|
+
*/
|
|
230
|
+
export function estimateRowSize(record: sync_rules.ToastableSqliteRow | undefined) {
|
|
231
|
+
if (record == null) {
|
|
232
|
+
return 12;
|
|
233
|
+
}
|
|
234
|
+
let size = 0;
|
|
235
|
+
for (let [key, value] of Object.entries(record)) {
|
|
236
|
+
size += 12 + key.length;
|
|
237
|
+
// number | string | null | bigint | Uint8Array
|
|
238
|
+
if (value == null) {
|
|
239
|
+
size += 4;
|
|
240
|
+
} else if (typeof value == 'number') {
|
|
241
|
+
size += 8;
|
|
242
|
+
} else if (typeof value == 'bigint') {
|
|
243
|
+
size += 8;
|
|
244
|
+
} else if (typeof value == 'string') {
|
|
245
|
+
size += value.length;
|
|
246
|
+
} else if (value instanceof Uint8Array) {
|
|
247
|
+
size += value.byteLength;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return size;
|
|
251
|
+
}
|
package/test/src/auth.test.ts
CHANGED
|
@@ -284,11 +284,23 @@ describe('JWT Auth', () => {
|
|
|
284
284
|
expect(errors).toEqual([]);
|
|
285
285
|
expect(keys.length).toBeGreaterThanOrEqual(1);
|
|
286
286
|
|
|
287
|
-
//
|
|
288
|
-
const invalid = new RemoteJWKSCollector('https://
|
|
289
|
-
|
|
287
|
+
// Domain names are resolved when retrieving keys
|
|
288
|
+
const invalid = new RemoteJWKSCollector('https://localhost/.well-known/jwks.json', {
|
|
289
|
+
lookupOptions: {
|
|
290
|
+
reject_ip_ranges: ['local']
|
|
291
|
+
}
|
|
290
292
|
});
|
|
291
293
|
expect(invalid.getKeys()).rejects.toThrow('IPs in this range are not supported');
|
|
294
|
+
|
|
295
|
+
// IPS throw an error immediately
|
|
296
|
+
expect(
|
|
297
|
+
() =>
|
|
298
|
+
new RemoteJWKSCollector('https://127.0.0.1/.well-known/jwks.json', {
|
|
299
|
+
lookupOptions: {
|
|
300
|
+
reject_ip_ranges: ['local']
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
).toThrowError('IPs in this range are not supported');
|
|
292
304
|
});
|
|
293
305
|
|
|
294
306
|
test('http not blocking local IPs', async () => {
|
|
@@ -301,10 +313,13 @@ describe('JWT Auth', () => {
|
|
|
301
313
|
expect(errors).toEqual([]);
|
|
302
314
|
expect(keys.length).toBeGreaterThanOrEqual(1);
|
|
303
315
|
|
|
304
|
-
// The localhost hostname fails to resolve correctly on MacOS https://github.com/nodejs/help/issues/2163
|
|
305
316
|
const invalid = new RemoteJWKSCollector('https://127.0.0.1/.well-known/jwks.json');
|
|
306
317
|
// Should try and fetch
|
|
307
|
-
expect(invalid.getKeys()).rejects.toThrow(
|
|
318
|
+
expect(invalid.getKeys()).rejects.toThrow();
|
|
319
|
+
|
|
320
|
+
const invalid2 = new RemoteJWKSCollector('https://localhost/.well-known/jwks.json');
|
|
321
|
+
// Should try and fetch
|
|
322
|
+
expect(invalid2.getKeys()).rejects.toThrow();
|
|
308
323
|
});
|
|
309
324
|
|
|
310
325
|
test('caching', async () => {
|