castle-web-sdk 0.4.0 → 0.4.2
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/dist/auth.d.ts +8 -0
- package/dist/auth.js +52 -0
- package/dist/castle.d.ts +11 -0
- package/dist/castle.js +8 -0
- package/dist/context.d.ts +30 -0
- package/dist/context.js +86 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +16 -0
- package/dist/graphql.d.ts +15 -0
- package/dist/graphql.js +120 -0
- package/dist/leaderboard.d.ts +20 -0
- package/dist/leaderboard.js +296 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +288 -0
- package/dist/storage.d.ts +17 -0
- package/dist/storage.js +405 -0
- package/dist/time.d.ts +17 -0
- package/dist/time.js +131 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +1 -0
- package/dist/user.d.ts +9 -0
- package/dist/user.js +58 -0
- package/package.json +30 -3
- package/src/auth.ts +64 -0
- package/src/castle.ts +19 -0
- package/src/context.ts +124 -0
- package/src/errors.ts +32 -0
- package/src/graphql.ts +182 -0
- package/src/leaderboard.ts +456 -0
- package/src/runtime.ts +345 -0
- package/src/storage.ts +636 -0
- package/src/time.ts +226 -0
- package/src/types.ts +7 -0
- package/src/user.ts +91 -0
- package/AGENTS.md +0 -27
- package/castle.js +0 -116
package/src/time.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { CastleError } from "./errors";
|
|
2
|
+
import { graphqlRequest } from "./graphql";
|
|
3
|
+
import type { Json } from "./types";
|
|
4
|
+
|
|
5
|
+
export type CastleClockZone = "player" | "Castle";
|
|
6
|
+
|
|
7
|
+
export interface CastleDateParts {
|
|
8
|
+
sec: number;
|
|
9
|
+
min: number;
|
|
10
|
+
hour: number;
|
|
11
|
+
day: number;
|
|
12
|
+
month: number;
|
|
13
|
+
year: number;
|
|
14
|
+
wday: number;
|
|
15
|
+
yday: number;
|
|
16
|
+
daysSinceCastleEpoch: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CastleTimeApi {
|
|
20
|
+
getServerTime(): Promise<number>;
|
|
21
|
+
getServerDate(timezone?: CastleClockZone): Promise<CastleDateParts>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ServerTimeQueryData {
|
|
25
|
+
serverTime: {
|
|
26
|
+
timestamp: number;
|
|
27
|
+
timezoneOffset: number;
|
|
28
|
+
castleEpochData: Json;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ServerTimeSnapshot {
|
|
33
|
+
offsetSeconds: number;
|
|
34
|
+
castleTimezoneOffsetMinutes: number;
|
|
35
|
+
daysSinceCastleEpoch: Record<CastleClockZone, number>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SERVER_TIME_QUERY = `
|
|
39
|
+
query CastleServerTime {
|
|
40
|
+
serverTime {
|
|
41
|
+
timestamp
|
|
42
|
+
timezoneOffset
|
|
43
|
+
castleEpochData
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
let serverTimeSnapshot: ServerTimeSnapshot | null = null;
|
|
49
|
+
let serverTimePromise: Promise<ServerTimeSnapshot> | null = null;
|
|
50
|
+
|
|
51
|
+
export const Time: CastleTimeApi = {
|
|
52
|
+
getServerTime,
|
|
53
|
+
getServerDate,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async function getServerTime(): Promise<number> {
|
|
57
|
+
const snapshot = await syncServerTime();
|
|
58
|
+
return clientUnixSeconds() + snapshot.offsetSeconds;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getServerDate(
|
|
62
|
+
timezone: string = "Castle",
|
|
63
|
+
): Promise<CastleDateParts> {
|
|
64
|
+
const operation = "Time.getServerDate";
|
|
65
|
+
const zone = clockZone(timezone, operation);
|
|
66
|
+
const snapshot = await syncServerTime();
|
|
67
|
+
return dateParts(
|
|
68
|
+
clientUnixSeconds() + snapshot.offsetSeconds,
|
|
69
|
+
zone,
|
|
70
|
+
snapshot,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function syncServerTime(): Promise<ServerTimeSnapshot> {
|
|
75
|
+
if (serverTimeSnapshot) return serverTimeSnapshot;
|
|
76
|
+
if (!serverTimePromise) {
|
|
77
|
+
serverTimePromise = fetchServerTime().then((snapshot) => {
|
|
78
|
+
serverTimeSnapshot = snapshot;
|
|
79
|
+
return snapshot;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return serverTimePromise;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchServerTime(): Promise<ServerTimeSnapshot> {
|
|
86
|
+
const operation = "Time.getServerTime";
|
|
87
|
+
const data = await graphqlRequest<ServerTimeQueryData>(
|
|
88
|
+
SERVER_TIME_QUERY,
|
|
89
|
+
undefined,
|
|
90
|
+
{
|
|
91
|
+
operation,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
const timestamp = numberField(
|
|
95
|
+
data.serverTime.timestamp,
|
|
96
|
+
"serverTime.timestamp",
|
|
97
|
+
operation,
|
|
98
|
+
);
|
|
99
|
+
const timezoneOffset = numberField(
|
|
100
|
+
data.serverTime.timezoneOffset,
|
|
101
|
+
"serverTime.timezoneOffset",
|
|
102
|
+
operation,
|
|
103
|
+
);
|
|
104
|
+
const epochData = jsonObject(
|
|
105
|
+
data.serverTime.castleEpochData,
|
|
106
|
+
"serverTime.castleEpochData",
|
|
107
|
+
operation,
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
offsetSeconds: snappedOffset(timestamp - clientUnixSeconds()),
|
|
111
|
+
castleTimezoneOffsetMinutes: timezoneOffset,
|
|
112
|
+
daysSinceCastleEpoch: {
|
|
113
|
+
Castle: numberField(
|
|
114
|
+
epochData.daysSinceServerTz,
|
|
115
|
+
"castleEpochData.daysSinceServerTz",
|
|
116
|
+
operation,
|
|
117
|
+
),
|
|
118
|
+
player: numberField(
|
|
119
|
+
epochData.daysSinceUserTz,
|
|
120
|
+
"castleEpochData.daysSinceUserTz",
|
|
121
|
+
operation,
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dateParts(
|
|
128
|
+
unixSeconds: number,
|
|
129
|
+
timezone: CastleClockZone,
|
|
130
|
+
snapshot: ServerTimeSnapshot,
|
|
131
|
+
): CastleDateParts {
|
|
132
|
+
const shiftedSeconds =
|
|
133
|
+
timezone === "Castle"
|
|
134
|
+
? unixSeconds + snapshot.castleTimezoneOffsetMinutes * 60
|
|
135
|
+
: unixSeconds;
|
|
136
|
+
const date = new Date(shiftedSeconds * 1000);
|
|
137
|
+
return timezone === "Castle"
|
|
138
|
+
? utcDateParts(date, snapshot.daysSinceCastleEpoch.Castle)
|
|
139
|
+
: localDateParts(date, snapshot.daysSinceCastleEpoch.player);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function utcDateParts(
|
|
143
|
+
date: Date,
|
|
144
|
+
daysSinceCastleEpoch: number,
|
|
145
|
+
): CastleDateParts {
|
|
146
|
+
return {
|
|
147
|
+
sec: date.getUTCSeconds(),
|
|
148
|
+
min: date.getUTCMinutes(),
|
|
149
|
+
hour: date.getUTCHours(),
|
|
150
|
+
day: date.getUTCDate(),
|
|
151
|
+
month: date.getUTCMonth() + 1,
|
|
152
|
+
year: date.getUTCFullYear(),
|
|
153
|
+
wday: date.getUTCDay() + 1,
|
|
154
|
+
yday: dayOfYear(
|
|
155
|
+
date.getUTCFullYear(),
|
|
156
|
+
date.getUTCMonth(),
|
|
157
|
+
date.getUTCDate(),
|
|
158
|
+
),
|
|
159
|
+
daysSinceCastleEpoch,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function localDateParts(
|
|
164
|
+
date: Date,
|
|
165
|
+
daysSinceCastleEpoch: number,
|
|
166
|
+
): CastleDateParts {
|
|
167
|
+
return {
|
|
168
|
+
sec: date.getSeconds(),
|
|
169
|
+
min: date.getMinutes(),
|
|
170
|
+
hour: date.getHours(),
|
|
171
|
+
day: date.getDate(),
|
|
172
|
+
month: date.getMonth() + 1,
|
|
173
|
+
year: date.getFullYear(),
|
|
174
|
+
wday: date.getDay() + 1,
|
|
175
|
+
yday: dayOfYear(date.getFullYear(), date.getMonth(), date.getDate()),
|
|
176
|
+
daysSinceCastleEpoch,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function dayOfYear(year: number, monthIndex: number, day: number): number {
|
|
181
|
+
const start = Date.UTC(year, 0, 1);
|
|
182
|
+
const current = Date.UTC(year, monthIndex, day);
|
|
183
|
+
return Math.floor((current - start) / 86400000) + 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clockZone(timezone: string, operation: string): CastleClockZone {
|
|
187
|
+
const normalized = timezone.trim().toLowerCase();
|
|
188
|
+
if (normalized === "castle") return "Castle";
|
|
189
|
+
if (normalized === "player") return "player";
|
|
190
|
+
throw new CastleError({
|
|
191
|
+
code: "UNSUPPORTED_TIMEZONE",
|
|
192
|
+
message: 'Time.getServerDate timezone must be "Castle" or "player".',
|
|
193
|
+
operation,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function clientUnixSeconds(): number {
|
|
198
|
+
return Math.floor(Date.now() / 1000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function snappedOffset(offsetSeconds: number): number {
|
|
202
|
+
return Math.abs(offsetSeconds) < 10 ? 0 : offsetSeconds;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function jsonObject(
|
|
206
|
+
value: Json,
|
|
207
|
+
field: string,
|
|
208
|
+
operation: string,
|
|
209
|
+
): Record<string, Json> {
|
|
210
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value))
|
|
211
|
+
return value;
|
|
212
|
+
throw new CastleError({
|
|
213
|
+
code: "GRAPHQL_BAD_DATA",
|
|
214
|
+
message: `Castle GraphQL field ${field} was not an object.`,
|
|
215
|
+
operation,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function numberField(value: Json, field: string, operation: string): number {
|
|
220
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
221
|
+
throw new CastleError({
|
|
222
|
+
code: "GRAPHQL_BAD_DATA",
|
|
223
|
+
message: `Castle GraphQL field ${field} was not a number.`,
|
|
224
|
+
operation,
|
|
225
|
+
});
|
|
226
|
+
}
|
package/src/types.ts
ADDED
package/src/user.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { requireAuthToken } from "./auth";
|
|
2
|
+
import { CastleError } from "./errors";
|
|
3
|
+
import { graphqlRequest } from "./graphql";
|
|
4
|
+
|
|
5
|
+
export interface CastleUser {
|
|
6
|
+
userId: string;
|
|
7
|
+
username: string;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CastleUserApi {
|
|
12
|
+
getCurrent(): Promise<CastleUser>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CurrentUserQueryData {
|
|
16
|
+
me: {
|
|
17
|
+
userId?: string | null;
|
|
18
|
+
username?: string | null;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CURRENT_USER_QUERY = `
|
|
23
|
+
query CastleCurrentUser {
|
|
24
|
+
me {
|
|
25
|
+
userId
|
|
26
|
+
username
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
let currentUser: CastleUser | null = null;
|
|
32
|
+
let currentUserPromise: Promise<CastleUser> | null = null;
|
|
33
|
+
|
|
34
|
+
export const User: CastleUserApi = {
|
|
35
|
+
getCurrent,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async function getCurrent(): Promise<CastleUser> {
|
|
39
|
+
if (currentUser) return currentUser;
|
|
40
|
+
currentUserPromise ??= fetchCurrentUser().then((user) => {
|
|
41
|
+
currentUser = user;
|
|
42
|
+
return user;
|
|
43
|
+
});
|
|
44
|
+
return currentUserPromise;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchCurrentUser(): Promise<CastleUser> {
|
|
48
|
+
const operation = "User.getCurrent";
|
|
49
|
+
const token = await requireAuthToken(operation);
|
|
50
|
+
const data = await graphqlRequest<CurrentUserQueryData>(
|
|
51
|
+
CURRENT_USER_QUERY,
|
|
52
|
+
undefined,
|
|
53
|
+
{
|
|
54
|
+
operation,
|
|
55
|
+
requireAuth: true,
|
|
56
|
+
token,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
if (!data.me) {
|
|
60
|
+
throw new CastleError({
|
|
61
|
+
code: "LOGIN_REQUIRED",
|
|
62
|
+
message: "Log in to Castle before using User.getCurrent().",
|
|
63
|
+
operation,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return normalizeCurrentUser(data.me, operation);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeCurrentUser(
|
|
70
|
+
user: NonNullable<CurrentUserQueryData["me"]>,
|
|
71
|
+
operation: string,
|
|
72
|
+
): CastleUser {
|
|
73
|
+
return {
|
|
74
|
+
userId: requiredString(user.userId, "me.userId", operation),
|
|
75
|
+
username: requiredString(user.username, "me.username", operation),
|
|
76
|
+
isActive: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function requiredString(
|
|
81
|
+
value: string | null | undefined,
|
|
82
|
+
field: string,
|
|
83
|
+
operation: string,
|
|
84
|
+
): string {
|
|
85
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
86
|
+
throw new CastleError({
|
|
87
|
+
code: "GRAPHQL_BAD_DATA",
|
|
88
|
+
message: `Castle GraphQL response did not include ${field}.`,
|
|
89
|
+
operation,
|
|
90
|
+
});
|
|
91
|
+
}
|
package/AGENTS.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Castle Web SDK
|
|
2
|
-
|
|
3
|
-
Import from `castle-web-sdk` (or `./castle.js` directly).
|
|
4
|
-
|
|
5
|
-
## API
|
|
6
|
-
|
|
7
|
-
### `setup()`
|
|
8
|
-
|
|
9
|
-
Connects to the CLI dev server for console log forwarding, screenshot capture, and live reload. Call once at startup.
|
|
10
|
-
|
|
11
|
-
```js
|
|
12
|
-
import { setup } from 'castle-web-sdk';
|
|
13
|
-
setup();
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
### `initCard()` -> HTMLElement
|
|
17
|
-
|
|
18
|
-
Creates a card-sized container (5:7 aspect ratio) that fills the viewport. Mount your game inside the returned element.
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
const card = initCard();
|
|
22
|
-
card.appendChild(myGameElement);
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Constants
|
|
26
|
-
|
|
27
|
-
- `CARD_RATIO` — `5 / 7`
|
package/castle.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Castle Web SDK
|
|
2
|
-
|
|
3
|
-
export const CARD_RATIO = 5 / 7;
|
|
4
|
-
|
|
5
|
-
let _ws = null;
|
|
6
|
-
let _logBuffer = [];
|
|
7
|
-
|
|
8
|
-
export function setup() {
|
|
9
|
-
_interceptConsole();
|
|
10
|
-
_connectLocal();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// --- Card shell ---
|
|
14
|
-
|
|
15
|
-
export function initCard() {
|
|
16
|
-
const style = document.createElement('style');
|
|
17
|
-
style.textContent = `
|
|
18
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
19
|
-
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
|
|
20
|
-
body { display: flex; align-items: center; justify-content: center; }
|
|
21
|
-
#castle-card {
|
|
22
|
-
position: relative;
|
|
23
|
-
border-radius: 4% / calc(4% * (5 / 7));
|
|
24
|
-
overflow: hidden;
|
|
25
|
-
background: #121213;
|
|
26
|
-
}
|
|
27
|
-
`;
|
|
28
|
-
document.head.appendChild(style);
|
|
29
|
-
|
|
30
|
-
const card = document.createElement('div');
|
|
31
|
-
card.id = 'castle-card';
|
|
32
|
-
document.body.appendChild(card);
|
|
33
|
-
|
|
34
|
-
function resize() {
|
|
35
|
-
if (window.CastleEmbed) {
|
|
36
|
-
card.style.width = '100vw';
|
|
37
|
-
card.style.height = '100vh';
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const maxW = 450, maxH = 630, pad = 20;
|
|
41
|
-
const aw = window.innerWidth - pad * 2;
|
|
42
|
-
const ah = window.innerHeight - pad * 2;
|
|
43
|
-
let w, h;
|
|
44
|
-
if (aw / ah < 5 / 7) { w = Math.min(aw, maxW); h = w * 7 / 5; }
|
|
45
|
-
else { h = Math.min(ah, maxH); w = h * 5 / 7; }
|
|
46
|
-
card.style.width = w + 'px';
|
|
47
|
-
card.style.height = h + 'px';
|
|
48
|
-
}
|
|
49
|
-
resize();
|
|
50
|
-
window.addEventListener('resize', resize);
|
|
51
|
-
|
|
52
|
-
return card;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// --- Console log forwarding ---
|
|
56
|
-
|
|
57
|
-
const _origLog = console.log;
|
|
58
|
-
const _origWarn = console.warn;
|
|
59
|
-
const _origError = console.error;
|
|
60
|
-
|
|
61
|
-
function _sendMsg(msg) {
|
|
62
|
-
if (_ws && _ws.readyState === WebSocket.OPEN) {
|
|
63
|
-
_ws.send(JSON.stringify(msg));
|
|
64
|
-
} else {
|
|
65
|
-
_logBuffer.push(msg);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function _interceptConsole() {
|
|
70
|
-
console.log = (...args) => { _origLog(...args); _sendMsg({ type: 'log', level: 'log', msg: args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ') }); };
|
|
71
|
-
console.warn = (...args) => { _origWarn(...args); _sendMsg({ type: 'log', level: 'warn', msg: args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ') }); };
|
|
72
|
-
console.error = (...args) => { _origError(...args); _sendMsg({ type: 'log', level: 'error', msg: args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ') }); };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// --- Screenshot ---
|
|
76
|
-
|
|
77
|
-
async function _captureScreenshot() {
|
|
78
|
-
const canvas = document.querySelector('canvas');
|
|
79
|
-
if (canvas) return canvas.toDataURL('image/png');
|
|
80
|
-
try {
|
|
81
|
-
const { default: html2canvas } = await import('https://esm.sh/html2canvas');
|
|
82
|
-
const c = await html2canvas(document.body);
|
|
83
|
-
return c.toDataURL('image/png');
|
|
84
|
-
} catch { return null; }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// --- Local WebSocket ---
|
|
88
|
-
|
|
89
|
-
function _connectLocal() {
|
|
90
|
-
fetch('/__castle/ws-port')
|
|
91
|
-
.then(r => r.json())
|
|
92
|
-
.then(({ port }) => {
|
|
93
|
-
if (!port) return;
|
|
94
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
95
|
-
ws.onopen = () => {
|
|
96
|
-
_ws = ws;
|
|
97
|
-
for (const msg of _logBuffer) _ws.send(JSON.stringify(msg));
|
|
98
|
-
_logBuffer = [];
|
|
99
|
-
};
|
|
100
|
-
ws.onmessage = (evt) => {
|
|
101
|
-
try {
|
|
102
|
-
const msg = JSON.parse(evt.data);
|
|
103
|
-
if (msg.type === 'screenshot_request') {
|
|
104
|
-
_captureScreenshot().then(data => {
|
|
105
|
-
if (data) _sendMsg({ type: 'screenshot_response', requestId: msg.requestId, data });
|
|
106
|
-
});
|
|
107
|
-
} else if (msg.type === 'restart') {
|
|
108
|
-
location.reload();
|
|
109
|
-
}
|
|
110
|
-
} catch {}
|
|
111
|
-
};
|
|
112
|
-
ws.onclose = () => { _ws = null; setTimeout(_connectLocal, 2000); };
|
|
113
|
-
ws.onerror = () => ws.close();
|
|
114
|
-
})
|
|
115
|
-
.catch(() => {});
|
|
116
|
-
}
|