castle-web-sdk 0.4.25 → 0.4.27
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 +253 -40
- package/dist/castle.d.ts +3 -0
- package/dist/castle.js +1 -0
- package/dist/chunk.d.ts +1 -0
- package/dist/chunk.js +15 -0
- package/dist/commands.d.ts +116 -1
- package/dist/commands.js +9 -0
- package/dist/lifecycle.d.ts +20 -0
- package/dist/lifecycle.js +7 -1
- package/dist/multiplayer.d.ts +6 -0
- package/dist/multiplayer.js +6 -0
- package/dist/resumeState.d.ts +2 -0
- package/dist/resumeState.js +84 -0
- package/dist/runtime.js +86 -12
- package/dist/server/platformHandle.d.ts +10 -0
- package/dist/server/storage.d.ts +46 -0
- package/dist/server/storage.js +218 -0
- package/dist/server/wrapper.d.ts +14 -1
- package/dist/server/wrapper.js +4 -1
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +29 -86
- package/dist/storageJson.d.ts +5 -0
- package/dist/storageJson.js +69 -0
- package/dist/store.d.ts +67 -0
- package/dist/store.js +313 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +39 -5
- package/dist/unloadFlush.d.ts +3 -0
- package/dist/unloadFlush.js +30 -0
- package/dist/unloadHandshake.d.ts +6 -0
- package/dist/unloadHandshake.js +47 -0
- package/package.json +1 -1
package/dist/transport.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// - web → window.parent.postMessage; responses via 'message' events
|
|
8
8
|
// - local → the castle-web serve dev server, over runtime.ts's websocket
|
|
9
9
|
// Error reconstruction is uniform here so callers always get a CastleError.
|
|
10
|
-
import { CASTLE_SDK_PROTOCOL, isResponseEnvelope, } from "./commands";
|
|
10
|
+
import { CASTLE_SDK_PROTOCOL, isResponseEnvelope, unloadRequestId, } from "./commands";
|
|
11
11
|
import { getCastleEmbed } from "./context";
|
|
12
12
|
import { CastleError } from "./errors";
|
|
13
13
|
import { sendLocalCommand } from "./runtime";
|
|
@@ -22,6 +22,7 @@ const INTERACTIVE_COMMANDS = new Set([
|
|
|
22
22
|
let nextRequestId = 1;
|
|
23
23
|
const pending = new Map();
|
|
24
24
|
let listenersInstalled = false;
|
|
25
|
+
let unloadHandler = null;
|
|
25
26
|
export async function hostRequest(command, params) {
|
|
26
27
|
try {
|
|
27
28
|
const channel = resolveChannel();
|
|
@@ -67,6 +68,26 @@ export function hostNotify(event) {
|
|
|
67
68
|
};
|
|
68
69
|
sendEnvelope(channel, envelope);
|
|
69
70
|
}
|
|
71
|
+
// Reply to a host unload request. One-way like hostNotify, but it carries the
|
|
72
|
+
// handshake's whole point, so it posts even with no state: the host is holding
|
|
73
|
+
// its teardown open until this arrives or its cap runs out.
|
|
74
|
+
export function postUnloadDone(unloadId, state) {
|
|
75
|
+
if (typeof window === "undefined")
|
|
76
|
+
return;
|
|
77
|
+
const channel = resolveChannel();
|
|
78
|
+
if (channel === "local")
|
|
79
|
+
return;
|
|
80
|
+
const envelope = {
|
|
81
|
+
castleSdk: CASTLE_SDK_PROTOCOL,
|
|
82
|
+
unloadDone: state === undefined ? { unloadId } : { unloadId, state },
|
|
83
|
+
};
|
|
84
|
+
sendEnvelope(channel, envelope);
|
|
85
|
+
}
|
|
86
|
+
// Registered by unloadHandshake.ts rather than imported from it, so this module
|
|
87
|
+
// keeps no dependency on the one that depends on it.
|
|
88
|
+
export function setUnloadHandler(handler) {
|
|
89
|
+
unloadHandler = handler;
|
|
90
|
+
}
|
|
70
91
|
function resolveChannel() {
|
|
71
92
|
if (typeof window === "undefined")
|
|
72
93
|
return "local";
|
|
@@ -82,7 +103,7 @@ function resolveChannel() {
|
|
|
82
103
|
return window.parent && window.parent !== window ? "web" : "local";
|
|
83
104
|
}
|
|
84
105
|
function postCommand(channel, command, params) {
|
|
85
|
-
|
|
106
|
+
installHostListener();
|
|
86
107
|
const requestId = `csdk_${nextRequestId++}`;
|
|
87
108
|
return new Promise((resolve, reject) => {
|
|
88
109
|
const timeout = INTERACTIVE_COMMANDS.has(command)
|
|
@@ -106,15 +127,28 @@ function sendEnvelope(channel, envelope) {
|
|
|
106
127
|
}
|
|
107
128
|
// The mobile host can't dispatch a DOM 'message' event, so it calls this global
|
|
108
129
|
// directly with the parsed envelope. The web host posts a 'message' event.
|
|
109
|
-
|
|
130
|
+
//
|
|
131
|
+
// Exported because a deck that only registers an unload subscriber never posts a
|
|
132
|
+
// command, and without this the global would never exist — the host's inject
|
|
133
|
+
// would land on nothing and its handshake would run out its cap in silence.
|
|
134
|
+
export function installHostListener() {
|
|
110
135
|
if (listenersInstalled || typeof window === "undefined")
|
|
111
136
|
return;
|
|
112
137
|
listenersInstalled = true;
|
|
113
|
-
window.__castleSdkHost = { receive: (message) =>
|
|
138
|
+
window.__castleSdkHost = { receive: (message) => receive(message) };
|
|
114
139
|
window.addEventListener("message", (event) => {
|
|
115
|
-
|
|
140
|
+
receive(event.data);
|
|
116
141
|
});
|
|
117
142
|
}
|
|
143
|
+
function receive(message) {
|
|
144
|
+
if (isResponseEnvelope(message)) {
|
|
145
|
+
settle(message);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const unloadId = unloadRequestId(message);
|
|
149
|
+
if (unloadId !== null)
|
|
150
|
+
unloadHandler?.(unloadId);
|
|
151
|
+
}
|
|
118
152
|
function settle(message) {
|
|
119
153
|
if (!isResponseEnvelope(message))
|
|
120
154
|
return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Somewhere to hang "flush before the page goes away".
|
|
2
|
+
//
|
|
3
|
+
// Writes batch on a timer, so leaving the page inside that window would lose
|
|
4
|
+
// them, and a deck saving progress as someone quits is exactly when it matters.
|
|
5
|
+
// `pagehide` is the reliable signal on iOS, where unload often never fires, and
|
|
6
|
+
// visibilitychange covers backgrounding without a navigation.
|
|
7
|
+
//
|
|
8
|
+
// Listeners are registered on the first write, so a deck that never stores
|
|
9
|
+
// anything hooks nothing.
|
|
10
|
+
//
|
|
11
|
+
// Handlers are held in a Set, so registering on every write is fine as long as
|
|
12
|
+
// the caller passes a STABLE function reference. A fresh closure per call would
|
|
13
|
+
// accumulate, and every one of them would run at unload.
|
|
14
|
+
const handlers = new Set();
|
|
15
|
+
let hooked = false;
|
|
16
|
+
export function onUnloadFlush(handler) {
|
|
17
|
+
handlers.add(handler);
|
|
18
|
+
if (hooked || typeof window === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
hooked = true;
|
|
21
|
+
const flushAll = () => {
|
|
22
|
+
for (const flush of handlers)
|
|
23
|
+
flush();
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener("pagehide", flushAll);
|
|
26
|
+
document.addEventListener("visibilitychange", () => {
|
|
27
|
+
if (document.visibilityState === "hidden")
|
|
28
|
+
flushAll();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The host-driven half of "this page is about to go away".
|
|
2
|
+
//
|
|
3
|
+
// `unloadFlush.ts` is the browser-event half — pagehide / visibilitychange,
|
|
4
|
+
// which a deck's own document raises. This one is the host's: an RN WebView
|
|
5
|
+
// being torn down on a feed swipe never raises those reliably, so the mobile
|
|
6
|
+
// host sends an explicit unload request first and holds its teardown open,
|
|
7
|
+
// briefly, for the reply.
|
|
8
|
+
//
|
|
9
|
+
// Subscribers run synchronously and the reply goes out in the same tick. That
|
|
10
|
+
// is the contract that makes the host's wait short: it is waiting on one
|
|
11
|
+
// postMessage, not on whatever work a subscriber might want to do. A subscriber
|
|
12
|
+
// with host-bound work of its own must post it BEFORE returning — deck→host
|
|
13
|
+
// messages are FIFO per WebView, so anything posted first arrives first.
|
|
14
|
+
import { hostNotify, installHostListener, postUnloadDone, setUnloadHandler } from "./transport";
|
|
15
|
+
const subscribers = new Set();
|
|
16
|
+
let installed = false;
|
|
17
|
+
let interestSent = false;
|
|
18
|
+
export function onHostUnload(subscriber) {
|
|
19
|
+
subscribers.add(subscriber);
|
|
20
|
+
if (!installed) {
|
|
21
|
+
installed = true;
|
|
22
|
+
installHostListener();
|
|
23
|
+
setUnloadHandler(runHandshake);
|
|
24
|
+
}
|
|
25
|
+
return () => subscribers.delete(subscriber);
|
|
26
|
+
}
|
|
27
|
+
// Tells the host this deck has something to hand over, so it runs the handshake
|
|
28
|
+
// (and pays its bounded wait) for this deck instead of skipping it. Sent once;
|
|
29
|
+
// the host doesn't care which subscriber prompted it.
|
|
30
|
+
export function notifyUnloadInterest() {
|
|
31
|
+
if (interestSent)
|
|
32
|
+
return;
|
|
33
|
+
interestSent = true;
|
|
34
|
+
hostNotify("unloadInterest");
|
|
35
|
+
}
|
|
36
|
+
function runHandshake(unloadId) {
|
|
37
|
+
const ctx = {};
|
|
38
|
+
for (const subscriber of subscribers) {
|
|
39
|
+
try {
|
|
40
|
+
subscriber(ctx);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.warn("Castle unload subscriber threw.", error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
postUnloadDone(unloadId, ctx.state);
|
|
47
|
+
}
|