aws-appsync-subscription-link 2.3.0 → 2.4.1
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 +22 -0
- package/__tests__/link/realtime-subscription-handshake-link-test.ts +163 -2
- package/lib/index.d.ts +2 -2
- package/lib/realtime-subscription-handshake-link.d.ts +3 -3
- package/lib/realtime-subscription-handshake-link.js +17 -8
- package/lib/subscription-handshake-link.d.ts +2 -2
- package/lib/types/index.d.ts +4 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
<a name="2.4.1"></a>
|
|
7
|
+
## [2.4.1](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@2.4.0...aws-appsync-subscription-link@2.4.1) (2022-10-07)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **data:** port over Angular fix from master ([#733](https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/733)) ([0a38f28](https://github.com/awslabs/aws-mobile-appsync-sdk-js/commit/0a38f28))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<a name="2.4.0"></a>
|
|
18
|
+
# [2.4.0](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@2.3.0...aws-appsync-subscription-link@2.4.0) (2022-06-24)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* Add keepAliveTimeoutMs config for AppSync WebSocket link ([#725](https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/725)) ([c91e507](https://github.com/awslabs/aws-mobile-appsync-sdk-js/commit/c91e507))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
<a name="2.3.0"></a>
|
|
7
29
|
# [2.3.0](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@2.2.7...aws-appsync-subscription-link@2.3.0) (2022-05-02)
|
|
8
30
|
|
|
@@ -2,11 +2,14 @@ import { AUTH_TYPE } from "aws-appsync-auth-link";
|
|
|
2
2
|
import { execute } from "apollo-link";
|
|
3
3
|
import gql from 'graphql-tag';
|
|
4
4
|
import { AppSyncRealTimeSubscriptionHandshakeLink } from '../../src/realtime-subscription-handshake-link';
|
|
5
|
+
import { MESSAGE_TYPES } from "../../src/types";
|
|
6
|
+
import { v4 as uuid } from "uuid";
|
|
7
|
+
jest.mock('uuid', () => ({ v4: jest.fn() }));
|
|
5
8
|
|
|
6
9
|
const query = gql`subscription { someSubscription { aField } }`
|
|
7
10
|
|
|
8
11
|
class myWebSocket implements WebSocket {
|
|
9
|
-
binaryType: BinaryType;
|
|
12
|
+
binaryType: BinaryType;
|
|
10
13
|
bufferedAmount: number;
|
|
11
14
|
extensions: string;
|
|
12
15
|
onclose: (this: WebSocket, ev: CloseEvent) => any;
|
|
@@ -359,6 +362,164 @@ describe("RealTime subscription link", () => {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
});
|
|
362
|
-
})
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("Can use a custom keepAliveTimeoutMs", (done) => {
|
|
368
|
+
const id = "abcd-efgh-ijkl-mnop";
|
|
369
|
+
uuid.mockImplementationOnce(() => id);
|
|
370
|
+
|
|
371
|
+
expect.assertions(5);
|
|
372
|
+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
|
|
373
|
+
return "2019-11-13T18:47:04.733Z";
|
|
374
|
+
}));
|
|
375
|
+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
|
|
376
|
+
expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
|
|
377
|
+
expect(protocol).toBe('graphql-ws');
|
|
378
|
+
const socket = new myWebSocket();
|
|
379
|
+
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
socket.close = () => {};
|
|
382
|
+
socket.onopen.call(socket, (undefined as unknown as Event));
|
|
383
|
+
socket.send = (msg: string) => {
|
|
384
|
+
const { type } = JSON.parse(msg);
|
|
385
|
+
|
|
386
|
+
switch (type) {
|
|
387
|
+
case MESSAGE_TYPES.GQL_CONNECTION_INIT:
|
|
388
|
+
socket.onmessage.call(socket, {
|
|
389
|
+
data: JSON.stringify({
|
|
390
|
+
type: MESSAGE_TYPES.GQL_CONNECTION_ACK,
|
|
391
|
+
payload: {
|
|
392
|
+
connectionTimeoutMs: 99999,
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
} as MessageEvent);
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
socket.onmessage.call(socket, {
|
|
398
|
+
data: JSON.stringify({
|
|
399
|
+
id,
|
|
400
|
+
type: MESSAGE_TYPES.GQL_DATA,
|
|
401
|
+
payload: {
|
|
402
|
+
data: { something: 123 },
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
} as MessageEvent);
|
|
406
|
+
|
|
407
|
+
}, 100);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}, 100);
|
|
412
|
+
|
|
413
|
+
return socket;
|
|
414
|
+
});
|
|
415
|
+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
|
|
416
|
+
auth: {
|
|
417
|
+
type: AUTH_TYPE.API_KEY,
|
|
418
|
+
apiKey: 'xxxxx'
|
|
419
|
+
},
|
|
420
|
+
region: 'us-west-2',
|
|
421
|
+
url: 'https://apikeytest.testcustomdomain.com/graphql',
|
|
422
|
+
keepAliveTimeoutMs: 123456,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(link).toBeInstanceOf(AppSyncRealTimeSubscriptionHandshakeLink);
|
|
426
|
+
expect((link as any).keepAliveTimeout).toBe(123456);
|
|
427
|
+
|
|
428
|
+
const sub = execute(link, { query }).subscribe({
|
|
429
|
+
error: (err) => {
|
|
430
|
+
console.log(JSON.stringify(err));
|
|
431
|
+
fail();
|
|
432
|
+
},
|
|
433
|
+
next: (data) => {
|
|
434
|
+
expect((link as any).keepAliveTimeout).toBe(123456);
|
|
435
|
+
done();
|
|
436
|
+
sub.unsubscribe();
|
|
437
|
+
},
|
|
438
|
+
complete: () => {
|
|
439
|
+
console.log('done with this');
|
|
440
|
+
fail();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("Uses service-provided timeout when no custom keepAliveTimeoutMs is configured", (done) => {
|
|
447
|
+
const id = "abcd-efgh-ijkl-mnop";
|
|
448
|
+
uuid.mockImplementationOnce(() => id);
|
|
449
|
+
|
|
450
|
+
expect.assertions(5);
|
|
451
|
+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
|
|
452
|
+
return "2019-11-13T18:47:04.733Z";
|
|
453
|
+
}));
|
|
454
|
+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
|
|
455
|
+
expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
|
|
456
|
+
expect(protocol).toBe('graphql-ws');
|
|
457
|
+
const socket = new myWebSocket();
|
|
458
|
+
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
socket.close = () => {};
|
|
461
|
+
socket.onopen.call(socket, (undefined as unknown as Event));
|
|
462
|
+
socket.send = (msg: string) => {
|
|
463
|
+
const { type } = JSON.parse(msg);
|
|
464
|
+
|
|
465
|
+
switch (type) {
|
|
466
|
+
case MESSAGE_TYPES.GQL_CONNECTION_INIT:
|
|
467
|
+
socket.onmessage.call(socket, {
|
|
468
|
+
data: JSON.stringify({
|
|
469
|
+
type: MESSAGE_TYPES.GQL_CONNECTION_ACK,
|
|
470
|
+
payload: {
|
|
471
|
+
connectionTimeoutMs: 99999,
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
} as MessageEvent);
|
|
475
|
+
setTimeout(() => {
|
|
476
|
+
socket.onmessage.call(socket, {
|
|
477
|
+
data: JSON.stringify({
|
|
478
|
+
id,
|
|
479
|
+
type: MESSAGE_TYPES.GQL_DATA,
|
|
480
|
+
payload: {
|
|
481
|
+
data: { something: 123 },
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
} as MessageEvent);
|
|
485
|
+
|
|
486
|
+
}, 100);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}, 100);
|
|
491
|
+
|
|
492
|
+
return socket;
|
|
493
|
+
});
|
|
494
|
+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
|
|
495
|
+
auth: {
|
|
496
|
+
type: AUTH_TYPE.API_KEY,
|
|
497
|
+
apiKey: 'xxxxx'
|
|
498
|
+
},
|
|
499
|
+
region: 'us-west-2',
|
|
500
|
+
url: 'https://apikeytest.testcustomdomain.com/graphql',
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(link).toBeInstanceOf(AppSyncRealTimeSubscriptionHandshakeLink);
|
|
504
|
+
expect((link as any).keepAliveTimeout).toBeUndefined();
|
|
505
|
+
|
|
506
|
+
const sub = execute(link, { query }).subscribe({
|
|
507
|
+
error: (err) => {
|
|
508
|
+
console.log(JSON.stringify(err));
|
|
509
|
+
fail();
|
|
510
|
+
},
|
|
511
|
+
next: (data) => {
|
|
512
|
+
expect((link as any).keepAliveTimeout).toBe(99999);
|
|
513
|
+
done();
|
|
514
|
+
sub.unsubscribe();
|
|
515
|
+
},
|
|
516
|
+
complete: () => {
|
|
517
|
+
console.log('done with this');
|
|
518
|
+
fail();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
363
524
|
|
|
364
525
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CONTROL_EVENTS_KEY } from "./subscription-handshake-link";
|
|
2
2
|
import { ApolloLink } from "apollo-link";
|
|
3
|
-
import {
|
|
4
|
-
declare function createSubscriptionHandshakeLink(args:
|
|
3
|
+
import { AppSyncRealTimeSubscriptionConfig } from "./types";
|
|
4
|
+
declare function createSubscriptionHandshakeLink(args: AppSyncRealTimeSubscriptionConfig, resultsFetcherLink?: ApolloLink): ApolloLink;
|
|
5
5
|
declare function createSubscriptionHandshakeLink(url: string, resultsFetcherLink?: ApolloLink): ApolloLink;
|
|
6
6
|
export { CONTROL_EVENTS_KEY, createSubscriptionHandshakeLink };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { ApolloLink, Observable, Operation, FetchResult } from "apollo-link";
|
|
6
|
-
import {
|
|
6
|
+
import { AppSyncRealTimeSubscriptionConfig } from "./types";
|
|
7
7
|
export declare const CONTROL_EVENTS_KEY = "@@controlEvents";
|
|
8
8
|
export declare class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
|
|
9
9
|
private url;
|
|
@@ -12,10 +12,10 @@ export declare class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink
|
|
|
12
12
|
private awsRealTimeSocket;
|
|
13
13
|
private socketStatus;
|
|
14
14
|
private keepAliveTimeoutId;
|
|
15
|
-
private keepAliveTimeout
|
|
15
|
+
private keepAliveTimeout?;
|
|
16
16
|
private subscriptionObserverMap;
|
|
17
17
|
private promiseArray;
|
|
18
|
-
constructor({ url: theUrl, region: theRegion, auth: theAuth }:
|
|
18
|
+
constructor({ url: theUrl, region: theRegion, auth: theAuth, keepAliveTimeoutMs }: AppSyncRealTimeSubscriptionConfig);
|
|
19
19
|
private isCustomDomain;
|
|
20
20
|
request(operation: Operation): Observable<FetchResult<{
|
|
21
21
|
[key: string]: any;
|
|
@@ -89,6 +89,10 @@ var CONNECTION_INIT_TIMEOUT = 15000;
|
|
|
89
89
|
* Time in milliseconds to wait for GQL_START_ACK message
|
|
90
90
|
*/
|
|
91
91
|
var START_ACK_TIMEOUT = 15000;
|
|
92
|
+
/**
|
|
93
|
+
* Frequency in milliseconds in which the server sends GQL_CONNECTION_KEEP_ALIVE messages
|
|
94
|
+
*/
|
|
95
|
+
var SERVER_KEEP_ALIVE_TIMEOUT = 1 * 60 * 1000;
|
|
92
96
|
/**
|
|
93
97
|
* Default Time in milliseconds to wait for GQL_CONNECTION_KEEP_ALIVE message
|
|
94
98
|
*/
|
|
@@ -98,15 +102,20 @@ var customDomainPath = '/realtime';
|
|
|
98
102
|
var AppSyncRealTimeSubscriptionHandshakeLink = /** @class */ (function (_super) {
|
|
99
103
|
__extends(AppSyncRealTimeSubscriptionHandshakeLink, _super);
|
|
100
104
|
function AppSyncRealTimeSubscriptionHandshakeLink(_a) {
|
|
101
|
-
var theUrl = _a.url, theRegion = _a.region, theAuth = _a.auth;
|
|
105
|
+
var theUrl = _a.url, theRegion = _a.region, theAuth = _a.auth, keepAliveTimeoutMs = _a.keepAliveTimeoutMs;
|
|
102
106
|
var _this = _super.call(this) || this;
|
|
103
107
|
_this.socketStatus = types_1.SOCKET_STATUS.CLOSED;
|
|
104
|
-
_this.keepAliveTimeout =
|
|
108
|
+
_this.keepAliveTimeout = undefined;
|
|
105
109
|
_this.subscriptionObserverMap = new Map();
|
|
106
110
|
_this.promiseArray = [];
|
|
107
111
|
_this.url = theUrl;
|
|
108
112
|
_this.region = theRegion;
|
|
109
113
|
_this.auth = theAuth;
|
|
114
|
+
_this.keepAliveTimeout = keepAliveTimeoutMs;
|
|
115
|
+
if (_this.keepAliveTimeout < SERVER_KEEP_ALIVE_TIMEOUT) {
|
|
116
|
+
var configName = 'keepAliveTimeoutMs';
|
|
117
|
+
throw new Error(configName + " must be greater than or equal to " + SERVER_KEEP_ALIVE_TIMEOUT + " (" + _this.keepAliveTimeout + " used).");
|
|
118
|
+
}
|
|
110
119
|
return _this;
|
|
111
120
|
}
|
|
112
121
|
// Check if url matches standard domain pattern
|
|
@@ -380,7 +389,7 @@ var AppSyncRealTimeSubscriptionHandshakeLink = /** @class */ (function (_super)
|
|
|
380
389
|
region: region,
|
|
381
390
|
credentials: credentials,
|
|
382
391
|
token: token,
|
|
383
|
-
graphql_headers: function () { }
|
|
392
|
+
graphql_headers: function () { }
|
|
384
393
|
})];
|
|
385
394
|
case 2:
|
|
386
395
|
headerString = _b.apply(_a, [_c.sent()]);
|
|
@@ -607,14 +616,14 @@ var AppSyncRealTimeSubscriptionHandshakeLink = /** @class */ (function (_super)
|
|
|
607
616
|
rej(new Error(JSON.stringify(event)));
|
|
608
617
|
};
|
|
609
618
|
_this.awsRealTimeSocket.onmessage = function (message) {
|
|
619
|
+
var _a;
|
|
610
620
|
logger("subscription message from AWS AppSyncRealTime: " + message.data + " ");
|
|
611
621
|
var data = JSON.parse(message.data);
|
|
612
|
-
var type = data.type,
|
|
622
|
+
var type = data.type, _b = data.payload, _c = (_b === void 0 ? {} : _b).connectionTimeoutMs, connectionTimeoutMs = _c === void 0 ? DEFAULT_KEEP_ALIVE_TIMEOUT : _c;
|
|
613
623
|
if (type === types_1.MESSAGE_TYPES.GQL_CONNECTION_ACK) {
|
|
614
624
|
ackOk = true;
|
|
615
|
-
_this.keepAliveTimeout = connectionTimeoutMs;
|
|
616
|
-
_this.awsRealTimeSocket.onmessage =
|
|
617
|
-
_this._handleIncomingSubscriptionMessage.bind(_this);
|
|
625
|
+
_this.keepAliveTimeout = (_a = _this.keepAliveTimeout) !== null && _a !== void 0 ? _a : connectionTimeoutMs;
|
|
626
|
+
_this.awsRealTimeSocket.onmessage = _this._handleIncomingSubscriptionMessage.bind(_this);
|
|
618
627
|
_this.awsRealTimeSocket.onerror = function (err) {
|
|
619
628
|
logger(err);
|
|
620
629
|
_this._errorDisconnect(types_1.CONTROL_MSG.CONNECTION_CLOSED);
|
|
@@ -627,7 +636,7 @@ var AppSyncRealTimeSubscriptionHandshakeLink = /** @class */ (function (_super)
|
|
|
627
636
|
return;
|
|
628
637
|
}
|
|
629
638
|
if (type === types_1.MESSAGE_TYPES.GQL_CONNECTION_ERROR) {
|
|
630
|
-
var
|
|
639
|
+
var _d = data.payload, _e = (_d === void 0 ? {} : _d).errors, _f = (_e === void 0 ? [] : _e)[0], _g = _f === void 0 ? {} : _f, _h = _g.errorType, errorType = _h === void 0 ? "" : _h, _j = _g.errorCode, errorCode = _j === void 0 ? 0 : _j;
|
|
631
640
|
rej({ errorType: errorType, errorCode: errorCode });
|
|
632
641
|
}
|
|
633
642
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/// <reference types="zen-observable" />
|
|
2
1
|
/*!
|
|
3
2
|
* Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
4
|
*/
|
|
6
5
|
import { ApolloLink, Observable, Operation, FetchResult } from "apollo-link";
|
|
6
|
+
import * as ZenObservable from 'zen-observable-ts';
|
|
7
7
|
declare type MqttConnectionInfo = {
|
|
8
8
|
client: string;
|
|
9
9
|
url: string;
|
|
@@ -15,7 +15,7 @@ export declare class SubscriptionHandshakeLink extends ApolloLink {
|
|
|
15
15
|
private topicObservers;
|
|
16
16
|
private clientObservers;
|
|
17
17
|
constructor(subsInfoContextKey: any);
|
|
18
|
-
request(operation: Operation): Observable<
|
|
18
|
+
request(operation: Operation): Observable<FetchResult> | null;
|
|
19
19
|
connectNewClients(connectionInfo: MqttConnectionInfo[], observer: ZenObservable.Observer<FetchResult>, operation: Operation): Promise<any[]>;
|
|
20
20
|
connectNewClient(connectionInfo: MqttConnectionInfo, observer: ZenObservable.Observer<FetchResult>, selectionNames: string[]): Promise<any>;
|
|
21
21
|
subscribeToTopics<T>(client: any, topics: string[], observer: ZenObservable.Observer<T>): Promise<unknown[]>;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/// <reference types="zen-observable" />
|
|
2
1
|
import { AuthOptions } from "aws-appsync-auth-link";
|
|
2
|
+
import * as ZenObservable from 'zen-observable-ts';
|
|
3
3
|
export declare enum SUBSCRIPTION_STATUS {
|
|
4
4
|
PENDING = 0,
|
|
5
5
|
CONNECTED = 1,
|
|
@@ -72,6 +72,9 @@ export declare type UrlInfo = {
|
|
|
72
72
|
auth: AuthOptions;
|
|
73
73
|
region: string;
|
|
74
74
|
};
|
|
75
|
+
export declare type AppSyncRealTimeSubscriptionConfig = UrlInfo & {
|
|
76
|
+
keepAliveTimeoutMs?: number;
|
|
77
|
+
};
|
|
75
78
|
export declare type ObserverQuery = {
|
|
76
79
|
observer: ZenObservable.SubscriptionObserver<any>;
|
|
77
80
|
query: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-appsync-subscription-link",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "AWS Mobile AppSync SDK for JavaScript",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"apollo-link-retry": "2.2.7",
|
|
26
26
|
"aws-appsync-auth-link": "^2.0.8",
|
|
27
27
|
"debug": "2.6.9",
|
|
28
|
-
"url": "^0.11.0"
|
|
28
|
+
"url": "^0.11.0",
|
|
29
|
+
"zen-observable-ts": "^1.2.5"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@redux-offline/redux-offline": "2.5.2-native.0",
|