castle-web-sdk 0.4.6 → 0.4.8
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 +81 -5
- package/dist/castle.d.ts +4 -0
- package/dist/castle.js +2 -0
- package/dist/commands.d.ts +21 -0
- package/dist/context.d.ts +4 -0
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.js +13 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.js +35 -0
- package/dist/runtime.js +13 -17
- package/dist/transport.d.ts +2 -1
- package/dist/transport.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";
|
|
|
19
19
|
- [Time](#time)
|
|
20
20
|
- [User](#user)
|
|
21
21
|
- [Pass](#pass)
|
|
22
|
+
- [Portal](#portal)
|
|
23
|
+
- [Lifecycle](#lifecycle)
|
|
22
24
|
- [Setup](#setup)
|
|
23
25
|
- [CastleError](#castleerror)
|
|
24
26
|
|
|
@@ -223,6 +225,74 @@ if (status === "purchased" || status === "alreadyOwned") {
|
|
|
223
225
|
}
|
|
224
226
|
```
|
|
225
227
|
|
|
228
|
+
## Portal
|
|
229
|
+
|
|
230
|
+
A portal sends the player from this deck to another Castle deck,
|
|
231
|
+
referred to by its deck id.
|
|
232
|
+
|
|
233
|
+
### `Portal.open(deckId): Promise<PortalOpenResult>`
|
|
234
|
+
|
|
235
|
+
Sends the player to `deckId`: the Castle app swipes the feed to it, the
|
|
236
|
+
website opens its page. There's nowhere to go in the editor or dev server,
|
|
237
|
+
so there it resolves `unavailable`. `PortalOpenResult` has a `status`:
|
|
238
|
+
|
|
239
|
+
- `'navigating'` — the host is moving to the target deck. Treat `open` as
|
|
240
|
+
final: this deck is torn down as it navigates away, so don't rely on code
|
|
241
|
+
running afterward.
|
|
242
|
+
- `'unavailable'` — nowhere to navigate here (the editor or dev server).
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
portalButton.onclick = async () => {
|
|
246
|
+
const { status } = await Portal.open(nextDeckId);
|
|
247
|
+
if (status === "unavailable") {
|
|
248
|
+
// No feed to navigate here — fall back to your own affordance.
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `Portal.prefetch(deckId): Promise<PortalPrefetchResult>`
|
|
254
|
+
|
|
255
|
+
A best-effort hint that the player may soon `open(deckId)`. In the Castle app
|
|
256
|
+
the host warms that deck (fetching it now so a later `open` transitions without
|
|
257
|
+
a cold load); everywhere else it's a no-op. `PortalPrefetchResult`
|
|
258
|
+
has a `status`:
|
|
259
|
+
|
|
260
|
+
- `'prefetching'` — the host accepted the hint (or the deck was already warm).
|
|
261
|
+
- `'rejected'` — the host declined, e.g. this deck has already prefetched its
|
|
262
|
+
limit of upcoming decks. Prefetch a few likely destinations, not everything.
|
|
263
|
+
- `'unavailable'` — the host doesn't prefetch here (the website, editor, or dev
|
|
264
|
+
server).
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
link.addEventListener("pointerenter", () => {
|
|
268
|
+
Portal.prefetch(link.dataset.deckId);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Lifecycle
|
|
273
|
+
|
|
274
|
+
`Lifecycle` tells the host when the deck has painted its first frame, so
|
|
275
|
+
the Castle feed can reveal it right away instead of waiting out a fixed
|
|
276
|
+
delay.
|
|
277
|
+
|
|
278
|
+
### `Lifecycle.ready()`
|
|
279
|
+
|
|
280
|
+
Signal that the deck has painted its first presentable frame. Idempotent
|
|
281
|
+
— only the first call has any effect.
|
|
282
|
+
|
|
283
|
+
Kits already wire this up to fire as soon as the first frame paints; if
|
|
284
|
+
you change how the deck first renders, make sure it still fires there. If
|
|
285
|
+
nothing calls this, the feed reveals the deck on its own after a brief
|
|
286
|
+
timeout.
|
|
287
|
+
|
|
288
|
+
If you call it right after kicking off your initial render, wait for the
|
|
289
|
+
next paint first so the host doesn't reveal a blank frame:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
createRoot(root).render(<App />);
|
|
293
|
+
requestAnimationFrame(() => requestAnimationFrame(() => Lifecycle.ready()));
|
|
294
|
+
```
|
|
295
|
+
|
|
226
296
|
## Setup
|
|
227
297
|
|
|
228
298
|
Startup, editor-mode check, and a file-write call for editor UI.
|
|
@@ -231,8 +301,9 @@ Startup, editor-mode check, and a file-write call for editor UI.
|
|
|
231
301
|
|
|
232
302
|
Call this once at the start of the deck, before any other SDK call.
|
|
233
303
|
`setup()` initializes the SDK so the rest of the API is usable and
|
|
234
|
-
mounts
|
|
235
|
-
|
|
304
|
+
mounts a 5:7 Castle card around whatever the deck renders into `#root`
|
|
305
|
+
in play mode. The SDK preserves the card aspect ratio; Castle hosts own
|
|
306
|
+
the card's max size, placement, and surrounding padding.
|
|
236
307
|
While running locally with `castle-web serve`, it also forwards
|
|
237
308
|
`console` output to the CLI and reloads the page when `castle-web
|
|
238
309
|
restart` runs.
|
|
@@ -246,9 +317,10 @@ setup();
|
|
|
246
317
|
### `initCard(): HTMLDivElement`
|
|
247
318
|
|
|
248
319
|
Use this when the deck draws into a `<canvas>` (or anything else)
|
|
249
|
-
rather than into the React tree at `#root`. Returns a
|
|
250
|
-
|
|
251
|
-
|
|
320
|
+
rather than into the React tree at `#root`. Returns a `<div>` with the
|
|
321
|
+
standard Castle 5:7 card aspect ratio. The div resizes itself when the
|
|
322
|
+
window resizes. In Castle-hosted iframes/WebViews it fills the host's
|
|
323
|
+
available card frame.
|
|
252
324
|
|
|
253
325
|
```js
|
|
254
326
|
import { setup, initCard } from "castle-web-sdk";
|
|
@@ -264,6 +336,10 @@ card.appendChild(canvas);
|
|
|
264
336
|
If the deck mounts a React tree into `#root` instead, you don't need
|
|
265
337
|
`initCard()` — `setup()` already wraps `#root`'s children in a card.
|
|
266
338
|
|
|
339
|
+
Host-specific layout flags such as `CastleEmbed.feed` are deprecated.
|
|
340
|
+
New code should let Castle hosts provide the frame and let the SDK infer
|
|
341
|
+
whether it is running standalone or embedded.
|
|
342
|
+
|
|
267
343
|
### `CARD_RATIO`
|
|
268
344
|
|
|
269
345
|
The card aspect ratio (`5 / 7`). Use this if you need to size something
|
package/dist/castle.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export { isEdit } from "./context";
|
|
|
2
2
|
export { CastleError } from "./errors";
|
|
3
3
|
export { Leaderboard } from "./leaderboard";
|
|
4
4
|
export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardSort, } from "./leaderboard";
|
|
5
|
+
export { Lifecycle } from "./lifecycle";
|
|
6
|
+
export type { CastleLifecycleApi } from "./lifecycle";
|
|
5
7
|
export { Pass } from "./passes";
|
|
6
8
|
export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
|
|
9
|
+
export { Portal } from "./portal";
|
|
10
|
+
export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./portal";
|
|
7
11
|
export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
|
|
8
12
|
export { SharedStorage, Storage } from "./storage";
|
|
9
13
|
export { Time } from "./time";
|
package/dist/castle.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
export { isEdit } from "./context";
|
|
3
3
|
export { CastleError } from "./errors";
|
|
4
4
|
export { Leaderboard } from "./leaderboard";
|
|
5
|
+
export { Lifecycle } from "./lifecycle";
|
|
5
6
|
export { Pass } from "./passes";
|
|
7
|
+
export { Portal } from "./portal";
|
|
6
8
|
export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
|
|
7
9
|
export { SharedStorage, Storage } from "./storage";
|
|
8
10
|
export { Time } from "./time";
|
package/dist/commands.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export type PassOfferStatus = "purchased" | "alreadyOwned" | "cancelled" | "unav
|
|
|
24
24
|
export interface PassOfferResult {
|
|
25
25
|
status: PassOfferStatus;
|
|
26
26
|
}
|
|
27
|
+
export type PortalOpenStatus = "navigating" | "unavailable";
|
|
28
|
+
export interface PortalOpenResult {
|
|
29
|
+
status: PortalOpenStatus;
|
|
30
|
+
}
|
|
31
|
+
export type PortalPrefetchStatus = "prefetching" | "rejected" | "unavailable";
|
|
32
|
+
export interface PortalPrefetchResult {
|
|
33
|
+
status: PortalPrefetchStatus;
|
|
34
|
+
}
|
|
27
35
|
export interface CommandParams {
|
|
28
36
|
"deckStorage.load": Record<string, never>;
|
|
29
37
|
"deckStorage.update": {
|
|
@@ -57,6 +65,12 @@ export interface CommandParams {
|
|
|
57
65
|
"pass.offer": {
|
|
58
66
|
passId: string;
|
|
59
67
|
};
|
|
68
|
+
"portal.open": {
|
|
69
|
+
targetDeckId: string;
|
|
70
|
+
};
|
|
71
|
+
"portal.prefetch": {
|
|
72
|
+
targetDeckId: string;
|
|
73
|
+
};
|
|
60
74
|
}
|
|
61
75
|
export interface CommandResult {
|
|
62
76
|
"deckStorage.load": {
|
|
@@ -93,6 +107,8 @@ export interface CommandResult {
|
|
|
93
107
|
hasPass: boolean;
|
|
94
108
|
};
|
|
95
109
|
"pass.offer": PassOfferResult;
|
|
110
|
+
"portal.open": PortalOpenResult;
|
|
111
|
+
"portal.prefetch": PortalPrefetchResult;
|
|
96
112
|
}
|
|
97
113
|
export type CommandName = keyof CommandParams;
|
|
98
114
|
export interface SerializedCommandError {
|
|
@@ -115,3 +131,8 @@ export interface CommandResponseEnvelope {
|
|
|
115
131
|
error?: SerializedCommandError;
|
|
116
132
|
}
|
|
117
133
|
export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
|
|
134
|
+
export type LifecycleEvent = "ready";
|
|
135
|
+
export interface LifecycleEnvelope {
|
|
136
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
137
|
+
lifecycle: LifecycleEvent;
|
|
138
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export type CastleHost = "web" | "mobile" | "dev";
|
|
2
2
|
export interface CastleEmbed {
|
|
3
3
|
edit?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Hosts should not set this for new behavior. The SDK now infers
|
|
6
|
+
* host-framed vs standalone presentation from its runtime context.
|
|
7
|
+
*/
|
|
4
8
|
feed?: boolean;
|
|
5
9
|
host?: CastleHost;
|
|
6
10
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Deck-side lifecycle signals. The feed reveals the deck when it reports `ready`
|
|
2
|
+
// (its first presentable frame) instead of waiting out a fixed timer.
|
|
3
|
+
import { hostNotify } from "./transport";
|
|
4
|
+
let readySent = false;
|
|
5
|
+
function ready() {
|
|
6
|
+
if (readySent)
|
|
7
|
+
return;
|
|
8
|
+
readySent = true;
|
|
9
|
+
hostNotify("ready");
|
|
10
|
+
}
|
|
11
|
+
export const Lifecycle = {
|
|
12
|
+
ready,
|
|
13
|
+
};
|
package/dist/portal.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PortalOpenResult, PortalPrefetchResult } from "./commands";
|
|
2
|
+
export type { PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./commands";
|
|
3
|
+
export interface CastlePortalApi {
|
|
4
|
+
open(deckId: string): Promise<PortalOpenResult>;
|
|
5
|
+
prefetch(deckId: string): Promise<PortalPrefetchResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare const Portal: CastlePortalApi;
|
package/dist/portal.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Portal — a deck-facing capability for sending the player to another Castle
|
|
2
|
+
// deck ("following a portal"). The host owns the transition: in the Castle feed
|
|
3
|
+
// it swipes to the target deck (which may itself be a web deck or a normal
|
|
4
|
+
// engine deck); elsewhere (the website, the dev server, the editor) there's no
|
|
5
|
+
// feed to navigate, so it resolves `unavailable` and nothing happens.
|
|
6
|
+
//
|
|
7
|
+
// Navigation is effectively fire-and-forget: once the host accepts it, the
|
|
8
|
+
// source deck is torn down as the feed moves to the target, so a deck should
|
|
9
|
+
// not rely on code running after a successful `open`.
|
|
10
|
+
import { CastleError } from "./errors";
|
|
11
|
+
import { hostRequest } from "./transport";
|
|
12
|
+
export const Portal = {
|
|
13
|
+
open,
|
|
14
|
+
prefetch,
|
|
15
|
+
};
|
|
16
|
+
async function open(deckId) {
|
|
17
|
+
if (typeof deckId !== "string" || deckId.length === 0) {
|
|
18
|
+
throw new CastleError({
|
|
19
|
+
code: "INVALID_ARGUMENT",
|
|
20
|
+
message: "Portal.open requires a deckId.",
|
|
21
|
+
operation: "Portal.open",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return hostRequest("portal.open", { targetDeckId: deckId });
|
|
25
|
+
}
|
|
26
|
+
async function prefetch(deckId) {
|
|
27
|
+
if (typeof deckId !== "string" || deckId.length === 0) {
|
|
28
|
+
throw new CastleError({
|
|
29
|
+
code: "INVALID_ARGUMENT",
|
|
30
|
+
message: "Portal.prefetch requires a deckId.",
|
|
31
|
+
operation: "Portal.prefetch",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return hostRequest("portal.prefetch", { targetDeckId: deckId });
|
|
35
|
+
}
|
package/dist/runtime.js
CHANGED
|
@@ -48,11 +48,6 @@ export function initCard() {
|
|
|
48
48
|
card.id = "castle-card";
|
|
49
49
|
document.body.appendChild(card);
|
|
50
50
|
function resize() {
|
|
51
|
-
if (getCastleEmbed()?.feed === true) {
|
|
52
|
-
card.style.width = "100vw";
|
|
53
|
-
card.style.height = "100vh";
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
51
|
const { w, h } = computeCardSize();
|
|
57
52
|
card.style.width = w + "px";
|
|
58
53
|
card.style.height = h + "px";
|
|
@@ -61,14 +56,11 @@ export function initCard() {
|
|
|
61
56
|
window.addEventListener("resize", resize);
|
|
62
57
|
return card;
|
|
63
58
|
}
|
|
64
|
-
// Constrains whatever the deck renders into #root to a
|
|
65
|
-
//
|
|
66
|
-
// full viewport, so the card shell applies only to standalone play.
|
|
59
|
+
// Constrains whatever the deck renders into #root to a 5:7 card in play mode.
|
|
60
|
+
// Hosts own max size and padding; the SDK only preserves the card aspect ratio.
|
|
67
61
|
function initPlayCard() {
|
|
68
62
|
if (isEdit())
|
|
69
63
|
return;
|
|
70
|
-
if (getCastleEmbed()?.feed === true)
|
|
71
|
-
return;
|
|
72
64
|
const style = document.createElement("style");
|
|
73
65
|
style.textContent = `
|
|
74
66
|
html, body { background: #000; }
|
|
@@ -94,23 +86,27 @@ function initPlayCard() {
|
|
|
94
86
|
window.addEventListener("resize", resize);
|
|
95
87
|
}
|
|
96
88
|
function computeCardSize() {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const aw = window.innerWidth - pad * 2;
|
|
101
|
-
const ah = window.innerHeight - pad * 2;
|
|
89
|
+
const pad = shouldUseStandaloneChrome() ? 20 : 0;
|
|
90
|
+
const aw = Math.max(1, window.innerWidth - pad * 2);
|
|
91
|
+
const ah = Math.max(1, window.innerHeight - pad * 2);
|
|
102
92
|
let w;
|
|
103
93
|
let h;
|
|
104
94
|
if (aw / ah < CARD_RATIO) {
|
|
105
|
-
w =
|
|
95
|
+
w = aw;
|
|
106
96
|
h = w / CARD_RATIO;
|
|
107
97
|
}
|
|
108
98
|
else {
|
|
109
|
-
h =
|
|
99
|
+
h = ah;
|
|
110
100
|
w = h * CARD_RATIO;
|
|
111
101
|
}
|
|
112
102
|
return { w, h };
|
|
113
103
|
}
|
|
104
|
+
function shouldUseStandaloneChrome() {
|
|
105
|
+
if (window.parent !== window)
|
|
106
|
+
return false;
|
|
107
|
+
const embed = getCastleEmbed();
|
|
108
|
+
return embed?.host === "dev";
|
|
109
|
+
}
|
|
114
110
|
function sendMsg(msg) {
|
|
115
111
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
116
112
|
ws.send(JSON.stringify(msg));
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandName, type CommandParams, type CommandResult } from "./commands";
|
|
1
|
+
import { type CommandName, type CommandParams, type CommandResult, type LifecycleEvent } from "./commands";
|
|
2
2
|
type PostChannel = "mobile" | "web";
|
|
3
3
|
interface ReactNativeWebViewBridge {
|
|
4
4
|
postMessage: (message: string) => void;
|
|
@@ -13,4 +13,5 @@ declare global {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResult[C]>;
|
|
15
15
|
export declare function getCommandChannel(): PostChannel | "local";
|
|
16
|
+
export declare function hostNotify(event: LifecycleEvent): void;
|
|
16
17
|
export {};
|
package/dist/transport.js
CHANGED
|
@@ -17,6 +17,7 @@ const REQUEST_TIMEOUT_MS = 15000;
|
|
|
17
17
|
// is wrong for them: they get NO timeout and resolve only when the host replies.
|
|
18
18
|
const INTERACTIVE_COMMANDS = new Set([
|
|
19
19
|
"pass.offer",
|
|
20
|
+
"portal.open",
|
|
20
21
|
]);
|
|
21
22
|
let nextRequestId = 1;
|
|
22
23
|
const pending = new Map();
|
|
@@ -52,6 +53,20 @@ export async function hostRequest(command, params) {
|
|
|
52
53
|
export function getCommandChannel() {
|
|
53
54
|
return resolveChannel();
|
|
54
55
|
}
|
|
56
|
+
// Fire-and-forget counterpart to hostRequest: no reply, no timeout. The local
|
|
57
|
+
// dev server has no surface to react to lifecycle events, so it's skipped.
|
|
58
|
+
export function hostNotify(event) {
|
|
59
|
+
if (typeof window === "undefined")
|
|
60
|
+
return;
|
|
61
|
+
const channel = resolveChannel();
|
|
62
|
+
if (channel === "local")
|
|
63
|
+
return;
|
|
64
|
+
const envelope = {
|
|
65
|
+
castleSdk: CASTLE_SDK_PROTOCOL,
|
|
66
|
+
lifecycle: event,
|
|
67
|
+
};
|
|
68
|
+
sendEnvelope(channel, envelope);
|
|
69
|
+
}
|
|
55
70
|
function resolveChannel() {
|
|
56
71
|
if (typeof window === "undefined")
|
|
57
72
|
return "local";
|