castle-web-sdk 0.4.4 → 0.4.6
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/README.md +321 -0
- package/dist/castle.d.ts +2 -0
- package/dist/castle.js +1 -0
- package/dist/commands.d.ts +117 -0
- package/dist/commands.js +16 -0
- package/dist/context.d.ts +2 -19
- package/dist/context.js +0 -71
- package/dist/leaderboard.js +69 -114
- package/dist/passes.d.ts +7 -0
- package/dist/passes.js +84 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +89 -15
- package/dist/storage.d.ts +1 -2
- package/dist/storage.js +82 -198
- package/dist/time.js +5 -16
- package/dist/transport.d.ts +16 -0
- package/dist/transport.js +126 -0
- package/dist/user.js +6 -23
- package/package.json +7 -4
- package/dist/auth.d.ts +0 -8
- package/dist/auth.js +0 -52
- package/dist/graphql.d.ts +0 -15
- package/dist/graphql.js +0 -120
- package/src/auth.ts +0 -64
- package/src/castle.ts +0 -19
- package/src/context.ts +0 -124
- package/src/errors.ts +0 -32
- package/src/graphql.ts +0 -182
- package/src/leaderboard.ts +0 -456
- package/src/runtime.ts +0 -372
- package/src/storage.ts +0 -636
- package/src/time.ts +0 -226
- package/src/types.ts +0 -7
- package/src/user.ts +0 -91
package/dist/leaderboard.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isEdit, requireDeckId } from "./context";
|
|
1
|
+
import { isEdit } from "./context";
|
|
3
2
|
import { CastleError } from "./errors";
|
|
4
|
-
import {
|
|
3
|
+
import { hostRequest } from "./transport";
|
|
5
4
|
const LEADERBOARD_FLUSH_INTERVAL_MS = 5000;
|
|
5
|
+
// Host stamps deckId, so one deck session = one set of leaderboards; key by
|
|
6
|
+
// variable+scope only and keep the best high/low score per key until flush.
|
|
6
7
|
const leaderboardWrites = new Map();
|
|
7
8
|
let leaderboardFlushTimer = null;
|
|
8
9
|
let leaderboardFlushPromise = null;
|
|
@@ -18,32 +19,78 @@ export const Leaderboard = {
|
|
|
18
19
|
function writeLeaderboard(variable, score, options) {
|
|
19
20
|
if (isEdit())
|
|
20
21
|
return;
|
|
21
|
-
|
|
22
|
+
try {
|
|
23
|
+
bufferLeaderboardWrite(variable, score, options);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
reportLeaderboardError(error);
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
async function fetchLeaderboardData(variable, type, options) {
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
assertLeaderboardVariable(variable, "Leaderboard.fetch");
|
|
31
|
+
assertLeaderboardType(type, "Leaderboard.fetch");
|
|
32
|
+
const scope = leaderboardScope(options);
|
|
33
|
+
// If the deck has written a score for this variable+scope this session, send
|
|
34
|
+
// it so the host writes-and-reads via leaderboardV2 and the player's own
|
|
35
|
+
// score shows up immediately (mirrors getLeaderboard in
|
|
36
|
+
// core/src/leaderboards.cpp — presence of a buffered score, not dirtiness,
|
|
37
|
+
// gates the write-through). Otherwise a plain read of the settled board.
|
|
38
|
+
const record = leaderboardWrites.get(leaderboardWriteKey(variable, scope));
|
|
39
|
+
const score = record
|
|
40
|
+
? type === "high"
|
|
41
|
+
? record.highScore
|
|
42
|
+
: record.lowScore
|
|
43
|
+
: null;
|
|
44
|
+
const { leaderboard, currentUserId } = await hostRequest("leaderboard.fetch", {
|
|
45
|
+
variable,
|
|
46
|
+
type,
|
|
47
|
+
scope,
|
|
48
|
+
...(score === null ? {} : { score }),
|
|
49
|
+
});
|
|
50
|
+
if (record && score !== null) {
|
|
51
|
+
clearLeaderboardDirtyAfterFetch(record, type, score);
|
|
52
|
+
}
|
|
53
|
+
return normalizeLeaderboard(leaderboard, currentUserId);
|
|
54
|
+
}
|
|
55
|
+
// After a write-through fetch, clear the dirty flag for the side we just sent
|
|
56
|
+
// (so the periodic flush won't re-send it via saveVariableToLeaderboard). If
|
|
57
|
+
// the other side's buffered value matches what we sent (the common single-score
|
|
58
|
+
// case where high == low), clear it too. The equality guards skip clearing if a
|
|
59
|
+
// concurrent write bumped the buffered score while the fetch was in flight —
|
|
60
|
+
// that newer score still needs flushing. Mirrors leaderboards.cpp.
|
|
61
|
+
function clearLeaderboardDirtyAfterFetch(record, type, sentScore) {
|
|
62
|
+
if (type === "high") {
|
|
63
|
+
if (record.highScore === sentScore) {
|
|
64
|
+
record.isHighDirty = false;
|
|
65
|
+
if (record.lowScore === sentScore)
|
|
66
|
+
record.isLowDirty = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (record.lowScore === sentScore) {
|
|
71
|
+
record.isLowDirty = false;
|
|
72
|
+
if (record.highScore === sentScore)
|
|
73
|
+
record.isHighDirty = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
26
76
|
}
|
|
27
|
-
|
|
77
|
+
function bufferLeaderboardWrite(variable, score, options) {
|
|
28
78
|
assertLeaderboardVariable(variable, "Leaderboard.write");
|
|
29
79
|
assertLeaderboardScore(score, "Leaderboard.write");
|
|
30
|
-
const deckId = await requireDeckId("Leaderboard.write");
|
|
31
|
-
await requireAuthToken("Leaderboard.write");
|
|
32
80
|
const scope = leaderboardScope(options);
|
|
33
|
-
const key = leaderboardWriteKey(
|
|
81
|
+
const key = leaderboardWriteKey(variable, scope);
|
|
34
82
|
const record = leaderboardWrites.get(key);
|
|
35
83
|
if (record) {
|
|
36
84
|
updatePendingLeaderboardWrite(record, score);
|
|
37
85
|
}
|
|
38
86
|
else {
|
|
39
|
-
leaderboardWrites.set(key, newPendingLeaderboardWrite(
|
|
87
|
+
leaderboardWrites.set(key, newPendingLeaderboardWrite(variable, scope, score));
|
|
40
88
|
}
|
|
41
89
|
ensureLeaderboardUnloadFlush();
|
|
42
90
|
scheduleLeaderboardFlush();
|
|
43
91
|
}
|
|
44
|
-
function newPendingLeaderboardWrite(
|
|
92
|
+
function newPendingLeaderboardWrite(variable, scope, score) {
|
|
45
93
|
return {
|
|
46
|
-
deckId,
|
|
47
94
|
variable,
|
|
48
95
|
scope,
|
|
49
96
|
highScore: score,
|
|
@@ -89,12 +136,8 @@ async function flushLeaderboardWrites() {
|
|
|
89
136
|
return leaderboardFlushPromise;
|
|
90
137
|
}
|
|
91
138
|
async function flushLeaderboardWritesOnce() {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
return;
|
|
95
|
-
const token = await requireAuthToken("Leaderboard.write");
|
|
96
|
-
for (const job of jobs) {
|
|
97
|
-
await saveLeaderboardScore(job.record, job.score, token);
|
|
139
|
+
for (const job of leaderboardWriteJobs()) {
|
|
140
|
+
await saveLeaderboardScore(job.record, job.score);
|
|
98
141
|
markLeaderboardWriteClean(job);
|
|
99
142
|
}
|
|
100
143
|
}
|
|
@@ -131,104 +174,13 @@ function hasDirtyLeaderboardWrites() {
|
|
|
131
174
|
}
|
|
132
175
|
return false;
|
|
133
176
|
}
|
|
134
|
-
async function
|
|
135
|
-
|
|
136
|
-
assertLeaderboardType(type, operation);
|
|
137
|
-
const [deckId, token, auth] = await Promise.all([
|
|
138
|
-
requireDeckId(operation),
|
|
139
|
-
requireAuthToken(operation),
|
|
140
|
-
getAuth(),
|
|
141
|
-
]);
|
|
142
|
-
return {
|
|
143
|
-
deckId,
|
|
144
|
-
variable,
|
|
145
|
-
type,
|
|
146
|
-
scope: leaderboardScope(options),
|
|
147
|
-
token,
|
|
148
|
-
userId: auth.userId ?? null,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
async function fetchLeaderboard(request) {
|
|
152
|
-
const data = await graphqlRequest(`
|
|
153
|
-
query CastleLeaderboard(
|
|
154
|
-
$deckId: ID!
|
|
155
|
-
$variable: String!
|
|
156
|
-
$type: LeaderboardType!
|
|
157
|
-
$filter: LeaderboardFilter!
|
|
158
|
-
$includeFollowList: Boolean
|
|
159
|
-
$includeParties: Boolean
|
|
160
|
-
$scope: String
|
|
161
|
-
) {
|
|
162
|
-
leaderboard(
|
|
163
|
-
deckId: $deckId
|
|
164
|
-
variable: $variable
|
|
165
|
-
type: $type
|
|
166
|
-
filter: $filter
|
|
167
|
-
includeFollowList: $includeFollowList
|
|
168
|
-
includeParties: $includeParties
|
|
169
|
-
scope: $scope
|
|
170
|
-
) ${leaderboardFields()}
|
|
171
|
-
}
|
|
172
|
-
`, leaderboardVariables(request), {
|
|
173
|
-
operation: "CastleLeaderboard",
|
|
174
|
-
token: request.token,
|
|
175
|
-
requireAuth: true,
|
|
176
|
-
});
|
|
177
|
-
return data.leaderboard;
|
|
178
|
-
}
|
|
179
|
-
async function saveLeaderboardScore(record, score, token) {
|
|
180
|
-
await graphqlRequest(`
|
|
181
|
-
mutation CastleSaveVariableToLeaderboard(
|
|
182
|
-
$deckId: ID!
|
|
183
|
-
$variable: String!
|
|
184
|
-
$score: Float!
|
|
185
|
-
$scope: String
|
|
186
|
-
) {
|
|
187
|
-
saveVariableToLeaderboard(
|
|
188
|
-
deckId: $deckId
|
|
189
|
-
variable: $variable
|
|
190
|
-
score: $score
|
|
191
|
-
scope: $scope
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
`, {
|
|
195
|
-
deckId: record.deckId,
|
|
177
|
+
async function saveLeaderboardScore(record, score) {
|
|
178
|
+
await hostRequest("leaderboard.save", {
|
|
196
179
|
variable: record.variable,
|
|
197
180
|
score,
|
|
198
181
|
scope: record.scope,
|
|
199
|
-
}, {
|
|
200
|
-
operation: "CastleSaveVariableToLeaderboard",
|
|
201
|
-
token,
|
|
202
|
-
requireAuth: true,
|
|
203
182
|
});
|
|
204
183
|
}
|
|
205
|
-
function leaderboardVariables(request) {
|
|
206
|
-
return {
|
|
207
|
-
deckId: request.deckId,
|
|
208
|
-
variable: request.variable,
|
|
209
|
-
type: request.type,
|
|
210
|
-
filter: "dedupUsers",
|
|
211
|
-
includeFollowList: false,
|
|
212
|
-
includeParties: false,
|
|
213
|
-
scope: request.scope,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function leaderboardFields() {
|
|
217
|
-
return `{
|
|
218
|
-
yourScore { score }
|
|
219
|
-
list {
|
|
220
|
-
place
|
|
221
|
-
score
|
|
222
|
-
user {
|
|
223
|
-
userId
|
|
224
|
-
username
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}`;
|
|
228
|
-
}
|
|
229
|
-
function leaderboardScope(options) {
|
|
230
|
-
return options.scope ?? null;
|
|
231
|
-
}
|
|
232
184
|
function normalizeLeaderboard(leaderboard, userId) {
|
|
233
185
|
const list = (leaderboard.list ?? []).map(normalizeLeaderboardEntry);
|
|
234
186
|
const playerRank = playerRankFromList(list, userId);
|
|
@@ -261,8 +213,11 @@ function scoreNumber(value) {
|
|
|
261
213
|
const parsed = Number.parseFloat(value);
|
|
262
214
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
263
215
|
}
|
|
264
|
-
function leaderboardWriteKey(
|
|
265
|
-
return `${
|
|
216
|
+
function leaderboardWriteKey(variable, scope) {
|
|
217
|
+
return `${variable}${scope ? `::${scope}` : ""}`;
|
|
218
|
+
}
|
|
219
|
+
function leaderboardScope(options) {
|
|
220
|
+
return options.scope ?? null;
|
|
266
221
|
}
|
|
267
222
|
function assertLeaderboardVariable(variable, operation) {
|
|
268
223
|
if (variable.trim().length > 0)
|
package/dist/passes.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PassOfferResult } from "./commands";
|
|
2
|
+
export type { PassOfferResult, PassOfferStatus } from "./commands";
|
|
3
|
+
export interface CastlePassApi {
|
|
4
|
+
has(passId: string): Promise<boolean>;
|
|
5
|
+
offer(passId: string): Promise<PassOfferResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare const Pass: CastlePassApi;
|
package/dist/passes.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Pass — a deck-facing capability for selling a creator "pass" to the player.
|
|
2
|
+
//
|
|
3
|
+
// The deck stays capability-AGNOSTIC: it just offers a pass to the player and
|
|
4
|
+
// gets back one normalized outcome regardless of platform. The host decides what
|
|
5
|
+
// UI to show and whether a real transaction can happen:
|
|
6
|
+
// - mobile app : renders the native bricks purchase sheet over the deck
|
|
7
|
+
// - web player : shows an "open in the app" upsell, returns `unavailable`
|
|
8
|
+
// - dev CLI : no host UI surface, so the SDK shows a minimal in-page
|
|
9
|
+
// notice itself (kept deliberately tiny), returns `unavailable`
|
|
10
|
+
// There is no capability check for the deck to make — every platform returns a
|
|
11
|
+
// PassOfferResult, so a single code path handles them all.
|
|
12
|
+
import { CastleError } from "./errors";
|
|
13
|
+
import { getCommandChannel, hostRequest } from "./transport";
|
|
14
|
+
export const Pass = {
|
|
15
|
+
has,
|
|
16
|
+
offer,
|
|
17
|
+
};
|
|
18
|
+
// Pure read — does the current player already own this pass? No UI, every
|
|
19
|
+
// platform answers it the same way (a GraphQL query). Use it to gate content or
|
|
20
|
+
// to decide whether to bother calling `offer`.
|
|
21
|
+
async function has(passId) {
|
|
22
|
+
if (typeof passId !== "string" || passId.length === 0) {
|
|
23
|
+
throw new CastleError({
|
|
24
|
+
code: "INVALID_ARGUMENT",
|
|
25
|
+
message: "Pass.has requires a passId.",
|
|
26
|
+
operation: "Pass.has",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const { hasPass } = await hostRequest("pass.has", { passId });
|
|
30
|
+
return hasPass;
|
|
31
|
+
}
|
|
32
|
+
async function offer(passId) {
|
|
33
|
+
if (typeof passId !== "string" || passId.length === 0) {
|
|
34
|
+
throw new CastleError({
|
|
35
|
+
code: "INVALID_ARGUMENT",
|
|
36
|
+
message: "Pass.offer requires a passId.",
|
|
37
|
+
operation: "Pass.offer",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const result = await hostRequest("pass.offer", { passId });
|
|
41
|
+
// On the dev server there's no host chrome to explain why nothing happened,
|
|
42
|
+
// so surface a small built-in notice. The mobile/web hosts render their own
|
|
43
|
+
// UI, so the SDK stays silent there.
|
|
44
|
+
if (result.status === "unavailable" && getCommandChannel() === "local") {
|
|
45
|
+
showDevUnavailableNotice();
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
let devNoticeEl = null;
|
|
50
|
+
let devNoticeTimer = null;
|
|
51
|
+
// Minimal, dependency-free toast. Dev-only affordance — not the place for a
|
|
52
|
+
// designed purchase UI.
|
|
53
|
+
function showDevUnavailableNotice() {
|
|
54
|
+
if (typeof document === "undefined")
|
|
55
|
+
return;
|
|
56
|
+
if (!devNoticeEl) {
|
|
57
|
+
devNoticeEl = document.createElement("div");
|
|
58
|
+
devNoticeEl.textContent = "Passes can only be purchased in the Castle app.";
|
|
59
|
+
devNoticeEl.style.cssText = [
|
|
60
|
+
"position:fixed",
|
|
61
|
+
"left:50%",
|
|
62
|
+
"bottom:24px",
|
|
63
|
+
"transform:translateX(-50%)",
|
|
64
|
+
"max-width:80vw",
|
|
65
|
+
"padding:10px 16px",
|
|
66
|
+
"border-radius:8px",
|
|
67
|
+
"background:rgba(0,0,0,0.82)",
|
|
68
|
+
"color:#fff",
|
|
69
|
+
"font:500 13px/1.4 system-ui,sans-serif",
|
|
70
|
+
"text-align:center",
|
|
71
|
+
"z-index:2147483647",
|
|
72
|
+
"pointer-events:none",
|
|
73
|
+
"transition:opacity 0.3s ease",
|
|
74
|
+
].join(";");
|
|
75
|
+
document.body.appendChild(devNoticeEl);
|
|
76
|
+
}
|
|
77
|
+
devNoticeEl.style.opacity = "1";
|
|
78
|
+
if (devNoticeTimer)
|
|
79
|
+
clearTimeout(devNoticeTimer);
|
|
80
|
+
devNoticeTimer = setTimeout(() => {
|
|
81
|
+
if (devNoticeEl)
|
|
82
|
+
devNoticeEl.style.opacity = "0";
|
|
83
|
+
}, 3200);
|
|
84
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CommandName, type CommandParams, type CommandResponseEnvelope } from "./commands";
|
|
1
2
|
export declare const CARD_RATIO: number;
|
|
2
3
|
interface LocalResponse {
|
|
3
4
|
type: string;
|
|
@@ -9,5 +10,6 @@ interface LocalResponse {
|
|
|
9
10
|
export declare function setup(): void;
|
|
10
11
|
export declare function writeFile(path: string, contents: string): Promise<LocalResponse>;
|
|
11
12
|
export declare function initCard(): HTMLDivElement;
|
|
13
|
+
export declare function sendLocalCommand<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResponseEnvelope>;
|
|
12
14
|
export declare function onBeforeRestart(hook: () => void | Promise<void>): () => void;
|
|
13
15
|
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { CASTLE_SDK_PROTOCOL, } from "./commands";
|
|
1
2
|
import { getCastleEmbed, isEdit } from "./context";
|
|
2
3
|
export const CARD_RATIO = 5 / 7;
|
|
3
4
|
let ws = null;
|
|
4
5
|
let logBuffer = [];
|
|
5
6
|
let nextRequestId = 1;
|
|
6
7
|
const pendingRequests = new Map();
|
|
8
|
+
// Local-dev command channel: the `castle-web serve` dev server is the host, so
|
|
9
|
+
// SDK commands ride the same websocket runtime.ts already uses for
|
|
10
|
+
// logs/screenshots/restart. Correlated by requestId, separate from the
|
|
11
|
+
// screenshot/write_file request map above.
|
|
12
|
+
const COMMAND_TIMEOUT_MS = 15000;
|
|
13
|
+
const SOCKET_WAIT_TIMEOUT_MS = 10000;
|
|
14
|
+
const pendingCommands = new Map();
|
|
7
15
|
const origLog = console.log;
|
|
8
16
|
const origWarn = console.warn;
|
|
9
17
|
const origError = console.error;
|
|
@@ -126,6 +134,61 @@ function sendLocalRequest(msg) {
|
|
|
126
134
|
ws.send(JSON.stringify(request));
|
|
127
135
|
});
|
|
128
136
|
}
|
|
137
|
+
// Send an SDK command to the dev server and resolve with the raw response
|
|
138
|
+
// envelope (ok/data/error). transport.ts interprets it — error reconstruction
|
|
139
|
+
// stays uniform across all three channels there. Waits for the socket to open
|
|
140
|
+
// so a command issued during startup isn't dropped.
|
|
141
|
+
export function sendLocalCommand(command, params) {
|
|
142
|
+
const requestId = `cmd_${nextRequestId++}`;
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
pendingCommands.delete(requestId);
|
|
146
|
+
reject(new Error(`Timed out waiting for command ${command}.`));
|
|
147
|
+
}, COMMAND_TIMEOUT_MS);
|
|
148
|
+
pendingCommands.set(requestId, (env) => {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
pendingCommands.delete(requestId);
|
|
151
|
+
resolve(env);
|
|
152
|
+
});
|
|
153
|
+
waitForSocket()
|
|
154
|
+
.then((socket) => {
|
|
155
|
+
socket.send(JSON.stringify({
|
|
156
|
+
type: "castle_command",
|
|
157
|
+
castleSdk: CASTLE_SDK_PROTOCOL,
|
|
158
|
+
requestId,
|
|
159
|
+
command,
|
|
160
|
+
params,
|
|
161
|
+
}));
|
|
162
|
+
})
|
|
163
|
+
.catch((error) => {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
pendingCommands.delete(requestId);
|
|
166
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function waitForSocket() {
|
|
171
|
+
if (ws && ws.readyState === WebSocket.OPEN)
|
|
172
|
+
return Promise.resolve(ws);
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const start = Date.now();
|
|
175
|
+
const poll = setInterval(() => {
|
|
176
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
177
|
+
clearInterval(poll);
|
|
178
|
+
resolve(ws);
|
|
179
|
+
}
|
|
180
|
+
else if (Date.now() - start > SOCKET_WAIT_TIMEOUT_MS) {
|
|
181
|
+
clearInterval(poll);
|
|
182
|
+
reject(new Error("Castle dev server is not connected."));
|
|
183
|
+
}
|
|
184
|
+
}, 100);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function resolveLocalCommand(msg) {
|
|
188
|
+
const pending = pendingCommands.get(msg.requestId);
|
|
189
|
+
if (pending)
|
|
190
|
+
pending(msg);
|
|
191
|
+
}
|
|
129
192
|
function resolveLocalRequest(msg) {
|
|
130
193
|
if (!msg.requestId)
|
|
131
194
|
return false;
|
|
@@ -187,26 +250,34 @@ async function captureWithHtml2Canvas(target) {
|
|
|
187
250
|
return null;
|
|
188
251
|
}
|
|
189
252
|
}
|
|
253
|
+
function cropCanvasToCard(card, canvas) {
|
|
254
|
+
const cardRect = card.getBoundingClientRect();
|
|
255
|
+
const c = document.createElement("canvas");
|
|
256
|
+
c.width = cardRect.width * devicePixelRatio;
|
|
257
|
+
c.height = cardRect.height * devicePixelRatio;
|
|
258
|
+
const ctx = c.getContext("2d");
|
|
259
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
260
|
+
const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
|
|
261
|
+
const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
|
|
262
|
+
ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
|
|
263
|
+
return c.toDataURL("image/png");
|
|
264
|
+
}
|
|
190
265
|
async function captureScreenshot() {
|
|
191
|
-
const card = document.
|
|
192
|
-
|
|
266
|
+
const card = document.querySelector("#castle-card, [data-castle-card]");
|
|
267
|
+
if (card) {
|
|
268
|
+
const cardCanvas = card.querySelector("canvas");
|
|
269
|
+
if (cardCanvas)
|
|
270
|
+
return cropCanvasToCard(card, cardCanvas);
|
|
271
|
+
const cropped = await captureWithHtml2Canvas(card);
|
|
272
|
+
if (cropped)
|
|
273
|
+
return cropped;
|
|
274
|
+
}
|
|
193
275
|
if (document.body?.dataset.castleScreenshotTarget === "viewport") {
|
|
194
276
|
const viewportCapture = await captureWithHtml2Canvas(document.body);
|
|
195
277
|
if (viewportCapture)
|
|
196
278
|
return viewportCapture;
|
|
197
279
|
}
|
|
198
|
-
|
|
199
|
-
const cardRect = card.getBoundingClientRect();
|
|
200
|
-
const c = document.createElement("canvas");
|
|
201
|
-
c.width = cardRect.width * devicePixelRatio;
|
|
202
|
-
c.height = cardRect.height * devicePixelRatio;
|
|
203
|
-
const ctx = c.getContext("2d");
|
|
204
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
205
|
-
const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
|
|
206
|
-
const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
|
|
207
|
-
ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
|
|
208
|
-
return c.toDataURL("image/png");
|
|
209
|
-
}
|
|
280
|
+
const canvas = document.querySelector("canvas");
|
|
210
281
|
if (canvas)
|
|
211
282
|
return canvas.toDataURL("image/png");
|
|
212
283
|
return captureWithHtml2Canvas(card || document.body);
|
|
@@ -280,6 +351,9 @@ function handleLocalMessage(msg) {
|
|
|
280
351
|
else if (msg.type === "write_file_response") {
|
|
281
352
|
resolveLocalRequest(msg);
|
|
282
353
|
}
|
|
354
|
+
else if (msg.type === "castle_command_response") {
|
|
355
|
+
resolveLocalCommand(msg);
|
|
356
|
+
}
|
|
283
357
|
}
|
|
284
358
|
// Restart (from `castle-web restart` / task agents) is debounced so a burst
|
|
285
359
|
// of reload requests -- several tasks finishing close together -- produces
|
|
@@ -298,7 +372,7 @@ function scheduleRestart() {
|
|
|
298
372
|
restartTimer = setTimeout(() => {
|
|
299
373
|
void (async () => {
|
|
300
374
|
try {
|
|
301
|
-
await Promise.all([...beforeRestartHooks].map((hook) => hook()));
|
|
375
|
+
await Promise.all([...beforeRestartHooks].map(async (hook) => hook()));
|
|
302
376
|
}
|
|
303
377
|
catch {
|
|
304
378
|
// a failed flush shouldn't block the reload
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type SharedScope } from "./commands";
|
|
1
2
|
import type { Json } from "./types";
|
|
2
|
-
type SharedScope = "deck" | "user";
|
|
3
3
|
export interface StorageApi {
|
|
4
4
|
get<T extends Json = Json>(key: string): Promise<T | null>;
|
|
5
5
|
set(key: string, value: Json): void;
|
|
@@ -14,4 +14,3 @@ export interface SharedStorageApi {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const Storage: StorageApi;
|
|
16
16
|
export declare const SharedStorage: SharedStorageApi;
|
|
17
|
-
export {};
|