@arcadible/sdk 1.2.2 → 1.3.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/README.md +21 -10
- package/dist/bus/client.d.ts +2 -0
- package/dist/bus/client.js +6 -1
- package/dist/bus/server.d.ts +5 -2
- package/dist/bus/server.js +39 -2
- package/dist/client.d.ts +17 -0
- package/dist/client.js +14 -1
- package/dist/host.d.ts +4 -0
- package/dist/host.js +3 -1
- package/dist/lifecycle/client.d.ts +7 -0
- package/dist/lifecycle/client.js +88 -0
- package/dist/lifecycle/rules.d.ts +10 -0
- package/dist/lifecycle/rules.js +53 -0
- package/dist/lifecycle/server.d.ts +8 -0
- package/dist/lifecycle/server.js +32 -0
- package/dist/saves/rules.d.ts +0 -1
- package/dist/saves/rules.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
<h1 align="center">Arcadible SDK</h1>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
|
|
10
|
+
Arcadible's loading screen for your game, and saves that carry over from one version to the next.
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://developer.arcadible.com/references/arcadible-sdk">Docs</a> ·
|
|
15
|
+
<a href="https://developer.arcadible.com/build/initialization">Initialization</a> ·
|
|
15
16
|
<a href="https://developer.arcadible.com/build/saves">Saves</a> ·
|
|
16
17
|
<a href="https://arcadible.com">Arcadible</a> ·
|
|
17
18
|
<a href="https://studio.arcadible.com">Studio</a>
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
<br>
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
The Arcadible SDK is optional: a game runs on [Arcadible](https://arcadible.com) without it. With it, a game gets two things from the play page around it: its [initialization](https://developer.arcadible.com/build/initialization), bracketed, so Arcadible's loading screen shows the game's splash and progress until its first frame is drawn, and [saves](https://developer.arcadible.com/build/saves), kept by the play page, so the player's progress carries over from one version to the next.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
@@ -29,29 +30,39 @@ npm install @arcadible/sdk
|
|
|
29
30
|
|
|
30
31
|
The package is for a game with a build step, such as a Vite project. A game without one uses the [hosted script](https://developer.arcadible.com/references/arcadible-sdk#install) instead.
|
|
31
32
|
|
|
32
|
-
##
|
|
33
|
+
## Initialization
|
|
33
34
|
|
|
34
35
|
```js
|
|
35
36
|
import { arcadible } from "@arcadible/sdk";
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
await arcadible.initBegin(); // first thing in your game's code
|
|
39
|
+
arcadible.initProgress(0.5);
|
|
40
|
+
// … load, build the scene, and draw the first frame …
|
|
41
|
+
await arcadible.initEnd();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Call `initBegin()` first thing in your game's code, report how far along you are with `initProgress()`, from 0 to 1, if you can measure it, and call `initEnd()` once your first frame is drawn. Arcadible shows your splash and the progress until then, and `initEnd()` resolves once the loading screen has faded into your game.
|
|
45
|
+
|
|
46
|
+
## Save and load
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// once the saves have loaded
|
|
39
50
|
const saved = await arcadible.saves.read("progress");
|
|
40
51
|
const progress = saved ? JSON.parse(saved) : { level: 1 };
|
|
41
52
|
|
|
42
|
-
// at a checkpoint
|
|
53
|
+
// at a checkpoint
|
|
43
54
|
await arcadible.saves.write("progress", JSON.stringify(progress));
|
|
44
55
|
```
|
|
45
56
|
|
|
46
|
-
A save is a name holding one string. `
|
|
57
|
+
A save is a name holding one string. Once `initBegin()` has resolved, `readSync`, `listSync`, and `infoSync` answer at once from memory, for reads during play. Every call that fails rejects (or, for the sync calls, throws) with an `ArcadibleError`, whose `code` says what to do. `arcadible.saves.where` says who can read the saves back: every version of the game, only this build, or only until the page closes.
|
|
47
58
|
|
|
48
|
-
Off Arcadible, such as on your dev server, the package works
|
|
59
|
+
Off Arcadible, such as on your dev server, the package still works, with the same rules and limits: there's no loading screen, so `initEnd()` resolves at once, saves are kept in the game's own browser storage, `arcadible.saves.where` is `"build"`, and `arcadible.build` is `null`.
|
|
49
60
|
|
|
50
61
|
## Learn more
|
|
51
62
|
|
|
52
|
-
- [
|
|
53
|
-
- [`arcadible.saves`](https://developer.arcadible.com/references/arcadible-sdk/saves): every call, what it returns, and when it fails.
|
|
63
|
+
- [Initialization](https://developer.arcadible.com/build/initialization): where each call goes, progress, and what comes after.
|
|
54
64
|
- [Saves](https://developer.arcadible.com/build/saves): using saves well, across versions, channels, and tabs, and where they're kept.
|
|
65
|
+
- [Arcadible SDK](https://developer.arcadible.com/references/arcadible-sdk): including it, every call, the saves' included, and every code.
|
|
55
66
|
- [Limits](https://developer.arcadible.com/references/limits#saves): how many saves, and how large.
|
|
56
67
|
|
|
57
68
|
## License
|
package/dist/bus/client.d.ts
CHANGED
|
@@ -10,11 +10,13 @@ export type BusOptions = {
|
|
|
10
10
|
};
|
|
11
11
|
export type RequestOptions<T> = {
|
|
12
12
|
late?: (result: T) => void;
|
|
13
|
+
timeout?: number;
|
|
13
14
|
};
|
|
14
15
|
export type Bus = {
|
|
15
16
|
readonly welcomed: Promise<Welcome>;
|
|
16
17
|
request<T>(name: string, params: object | undefined, shape: Shape<T>, options?: RequestOptions<T>): Promise<T>;
|
|
17
18
|
on<T>(name: string, shape: Shape<T>, listener: (data: T) => void): () => void;
|
|
19
|
+
emit(name: string, data?: unknown): void;
|
|
18
20
|
};
|
|
19
21
|
export declare function mayHaveLanded(err: unknown): boolean;
|
|
20
22
|
export declare function openBus(options: BusOptions): Bus;
|
package/dist/bus/client.js
CHANGED
|
@@ -184,7 +184,7 @@ export function openBus(options) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
reject(unsure(Codes.Timeout, "The play page took too long to answer. Try again."));
|
|
187
|
-
}, requestTimeout);
|
|
187
|
+
}, requestOptions.timeout ?? requestTimeout);
|
|
188
188
|
pending.set(id, {
|
|
189
189
|
name,
|
|
190
190
|
resolve: resolve,
|
|
@@ -196,6 +196,11 @@ export function openBus(options) {
|
|
|
196
196
|
link.postMessage({ kind: "request", id, name, params });
|
|
197
197
|
});
|
|
198
198
|
},
|
|
199
|
+
emit(name, data) {
|
|
200
|
+
void welcomed.then(() => {
|
|
201
|
+
port?.postMessage({ kind: "event", name, data });
|
|
202
|
+
});
|
|
203
|
+
},
|
|
199
204
|
on(name, shape, listener) {
|
|
200
205
|
// each listener on its own: one that throws, or data it can't take,
|
|
201
206
|
// doesn't stop the others.
|
package/dist/bus/server.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { type Build, type Welcome } from "./protocol.js";
|
|
1
|
+
import { type Build, type Hello, type Welcome } from "./protocol.js";
|
|
2
2
|
export type Params = Record<string, unknown>;
|
|
3
3
|
export type Handler = {
|
|
4
4
|
run(params: Params): Promise<unknown>;
|
|
5
5
|
key?(params: Params): string;
|
|
6
6
|
coalesce?: boolean;
|
|
7
|
+
parallel?: boolean;
|
|
7
8
|
};
|
|
8
9
|
export type ModuleContext = {
|
|
9
10
|
broadcast(name: string, data: unknown): void;
|
|
10
11
|
};
|
|
11
12
|
export type Module = {
|
|
12
13
|
handlers: Record<string, Handler>;
|
|
14
|
+
events?: Record<string, (data: unknown) => void>;
|
|
13
15
|
open?(send: (name: string, data: unknown) => void): Promise<void>;
|
|
14
16
|
close?(): void;
|
|
15
17
|
};
|
|
@@ -23,11 +25,12 @@ export type Server = {
|
|
|
23
25
|
close(): void;
|
|
24
26
|
};
|
|
25
27
|
export declare function createServer(options: ServerOptions): Server;
|
|
26
|
-
export declare function answerHellos({ frame, origin, server, window: win, }: {
|
|
28
|
+
export declare function answerHellos({ frame, origin, server, window: win, onHello, }: {
|
|
27
29
|
frame: HTMLIFrameElement;
|
|
28
30
|
origin: string;
|
|
29
31
|
server: Server;
|
|
30
32
|
window: Window;
|
|
33
|
+
onHello?: ((hello: Hello) => void) | undefined;
|
|
31
34
|
}): {
|
|
32
35
|
close(): void;
|
|
33
36
|
};
|
package/dist/bus/server.js
CHANGED
|
@@ -54,10 +54,14 @@ export function createServer(options) {
|
|
|
54
54
|
},
|
|
55
55
|
}));
|
|
56
56
|
const handlers = new Map();
|
|
57
|
+
const events = new Map();
|
|
57
58
|
for (const module of modules) {
|
|
58
59
|
for (const [name, handler] of Object.entries(module.handlers)) {
|
|
59
60
|
handlers.set(name, handler);
|
|
60
61
|
}
|
|
62
|
+
for (const [name, listener] of Object.entries(module.events ?? {})) {
|
|
63
|
+
events.set(name, listener);
|
|
64
|
+
}
|
|
61
65
|
}
|
|
62
66
|
const refuse = function (port, id, error) {
|
|
63
67
|
port.postMessage({ kind: "response", id, error });
|
|
@@ -110,6 +114,12 @@ export function createServer(options) {
|
|
|
110
114
|
}
|
|
111
115
|
inFlight.set(port, (inFlight.get(port) ?? 0) + 1);
|
|
112
116
|
const entry = { name, port, params, ids: [id] };
|
|
117
|
+
if (handler.parallel) {
|
|
118
|
+
Promise.resolve()
|
|
119
|
+
.then(() => handler.run(entry.params))
|
|
120
|
+
.then((result) => respond(entry, { result }), (err) => respond(entry, { error: errorBody(err) }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
113
123
|
if (key !== null)
|
|
114
124
|
waiting.set(key, entry);
|
|
115
125
|
enqueue(() => {
|
|
@@ -118,8 +128,29 @@ export function createServer(options) {
|
|
|
118
128
|
return handler.run(entry.params);
|
|
119
129
|
}).then((result) => respond(entry, { result }), (err) => respond(entry, { error: errorBody(err) }));
|
|
120
130
|
};
|
|
131
|
+
// an event from the game: unknown ones, and ones too large, are dropped,
|
|
132
|
+
// since nothing waits on them.
|
|
133
|
+
const hear = function (name, data) {
|
|
134
|
+
const listener = events.get(name);
|
|
135
|
+
if (!listener)
|
|
136
|
+
return;
|
|
137
|
+
if (messageSize(data, MAX_MESSAGE) > MAX_MESSAGE)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
listener(data);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
log("error", `the game's ${name} failed.`, err);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
121
146
|
const receive = function (port, data) {
|
|
122
|
-
if (!isMessage(data)
|
|
147
|
+
if (!isMessage(data))
|
|
148
|
+
return;
|
|
149
|
+
if (data.kind === "event") {
|
|
150
|
+
hear(data.name, data.data);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (data.kind !== "request")
|
|
123
154
|
return;
|
|
124
155
|
if (messageSize(data, MAX_MESSAGE) > MAX_MESSAGE) {
|
|
125
156
|
refuse(port, data.id, {
|
|
@@ -170,7 +201,7 @@ export function createServer(options) {
|
|
|
170
201
|
// iframe, at its build's exact origin, with a welcome and a private
|
|
171
202
|
// channel. A new page load in the frame says hello again, and replaces the
|
|
172
203
|
// channel. Call it before inserting the iframe, so no hello is missed.
|
|
173
|
-
export function answerHellos({ frame, origin, server, window: win, }) {
|
|
204
|
+
export function answerHellos({ frame, origin, server, window: win, onHello, }) {
|
|
174
205
|
let current = null;
|
|
175
206
|
const onMessage = function (event) {
|
|
176
207
|
const target = frame.contentWindow;
|
|
@@ -180,6 +211,12 @@ export function answerHellos({ frame, origin, server, window: win, }) {
|
|
|
180
211
|
!isHello(event.data)) {
|
|
181
212
|
return;
|
|
182
213
|
}
|
|
214
|
+
try {
|
|
215
|
+
onHello?.(event.data);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
log("error", "the page's hello listener failed.", err);
|
|
219
|
+
}
|
|
183
220
|
const channel = new MessageChannel();
|
|
184
221
|
const welcome = server.connect(channel.port1);
|
|
185
222
|
if (current)
|
package/dist/client.d.ts
CHANGED
|
@@ -23,6 +23,23 @@ export interface Arcadible {
|
|
|
23
23
|
readonly build: Build | null | undefined;
|
|
24
24
|
/** Saves that carry over between the game's versions. */
|
|
25
25
|
readonly saves: Saves;
|
|
26
|
+
/**
|
|
27
|
+
* Begins the game's initialization: call it first, before anything you
|
|
28
|
+
* await, and Arcadible shows the game's loading screen until `initEnd()`.
|
|
29
|
+
* It resolves as `ready` does, once the SDK and the saves are ready.
|
|
30
|
+
*/
|
|
31
|
+
initBegin(): Promise<Ready>;
|
|
32
|
+
/**
|
|
33
|
+
* How far along the initialization is, from 0 to 1, measured however the
|
|
34
|
+
* game likes. Optional; a lower value than the last is ignored.
|
|
35
|
+
*/
|
|
36
|
+
initProgress(fraction: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* Ends the initialization, once the game's first frame is drawn:
|
|
39
|
+
* Arcadible takes its loading screen down, and this resolves once the
|
|
40
|
+
* game is visible.
|
|
41
|
+
*/
|
|
42
|
+
initEnd(): Promise<void>;
|
|
26
43
|
}
|
|
27
44
|
export type ClientOptions = {
|
|
28
45
|
window?: Window;
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// [import]
|
|
5
5
|
import { openBus } from "./bus/client.js";
|
|
6
6
|
import { buildOf, REQUEST_TIMEOUT } from "./bus/protocol.js";
|
|
7
|
+
import { createLifecycle } from "./lifecycle/client.js";
|
|
7
8
|
import { indexedDBBackend } from "./saves/backend/indexeddb.js";
|
|
8
9
|
import { createSaves } from "./saves/client.js";
|
|
9
10
|
import { serve } from "./host.js";
|
|
@@ -20,9 +21,10 @@ const STANDALONE = {
|
|
|
20
21
|
// [function]
|
|
21
22
|
export function createClient(options = {}) {
|
|
22
23
|
const backend = options.backend ?? (() => indexedDBBackend());
|
|
24
|
+
const win = options.window ?? (typeof window === "undefined" ? undefined : window);
|
|
23
25
|
const bus = openBus({
|
|
24
26
|
client: VERSION,
|
|
25
|
-
window:
|
|
27
|
+
window: win,
|
|
26
28
|
origins: options.origins,
|
|
27
29
|
handshakeTimeout: options.handshakeTimeout,
|
|
28
30
|
requestTimeout: options.requestTimeout,
|
|
@@ -37,6 +39,7 @@ export function createClient(options = {}) {
|
|
|
37
39
|
settled = { build: buildOf(welcome.build) };
|
|
38
40
|
return settled;
|
|
39
41
|
});
|
|
42
|
+
const lifecycle = createLifecycle(bus, win);
|
|
40
43
|
return {
|
|
41
44
|
version: VERSION,
|
|
42
45
|
ready,
|
|
@@ -44,5 +47,15 @@ export function createClient(options = {}) {
|
|
|
44
47
|
return settled ? settled.build : undefined;
|
|
45
48
|
},
|
|
46
49
|
saves,
|
|
50
|
+
initBegin() {
|
|
51
|
+
lifecycle.begin();
|
|
52
|
+
return ready;
|
|
53
|
+
},
|
|
54
|
+
initProgress(fraction) {
|
|
55
|
+
lifecycle.progress(fraction);
|
|
56
|
+
},
|
|
57
|
+
initEnd() {
|
|
58
|
+
return lifecycle.end();
|
|
59
|
+
},
|
|
47
60
|
};
|
|
48
61
|
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Hello } from "./bus/protocol.js";
|
|
1
2
|
import { type Server } from "./bus/server.js";
|
|
3
|
+
import { type LifecycleHost } from "./lifecycle/server.js";
|
|
2
4
|
import type { Backend } from "./saves/backend/types.js";
|
|
3
5
|
import type { Where } from "./saves/rules.js";
|
|
4
6
|
export type ServeOptions = {
|
|
@@ -8,11 +10,13 @@ export type ServeOptions = {
|
|
|
8
10
|
backend: Backend;
|
|
9
11
|
where?: Where;
|
|
10
12
|
now?: () => Date;
|
|
13
|
+
lifecycle?: LifecycleHost;
|
|
11
14
|
};
|
|
12
15
|
type HostOptions = ServeOptions & {
|
|
13
16
|
frame: HTMLIFrameElement;
|
|
14
17
|
origin: string;
|
|
15
18
|
window?: Window;
|
|
19
|
+
onHello?: (hello: Hello) => void;
|
|
16
20
|
};
|
|
17
21
|
export declare function serve(options: ServeOptions): Server;
|
|
18
22
|
export declare function createHost(options: HostOptions): {
|
package/dist/host.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The page's side of the bus. The play page uses `createHost`;
|
|
2
2
|
// a game standing alone runs `serve` in its own page.
|
|
3
|
-
// [import]
|
|
4
3
|
import { answerHellos, createServer } from "./bus/server.js";
|
|
4
|
+
import { lifecycleServer } from "./lifecycle/server.js";
|
|
5
5
|
import { savesServer } from "./saves/server.js";
|
|
6
6
|
// [function]
|
|
7
7
|
// Answers the bus for one game in one channel, on any number of ports.
|
|
@@ -17,6 +17,7 @@ export function serve(options) {
|
|
|
17
17
|
version,
|
|
18
18
|
where: options.where ?? "game",
|
|
19
19
|
}),
|
|
20
|
+
...(options.lifecycle ? [lifecycleServer(options.lifecycle)] : []),
|
|
20
21
|
],
|
|
21
22
|
});
|
|
22
23
|
}
|
|
@@ -30,6 +31,7 @@ export function createHost(options) {
|
|
|
30
31
|
origin: options.origin,
|
|
31
32
|
server: serve(options),
|
|
32
33
|
window: options.window ?? window,
|
|
34
|
+
onHello: options.onHello,
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
export { indexedDBBackend } from "./saves/backend/indexeddb.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The game's side of its initialization: `initBegin`, `initProgress`, and
|
|
2
|
+
// `initEnd` on `arcadible` (DESIGN.md, section 7, Initialization). The page
|
|
3
|
+
// keeps its loading screen only for a game that began before its own `load`,
|
|
4
|
+
// which this side reports, once, as it happens, so the page decides without
|
|
5
|
+
// a timer. Calls after that do nothing, and the console says why once.
|
|
6
|
+
import { isObject, log } from "../bus/protocol.js";
|
|
7
|
+
import { END_TIMEOUT, fractionOf, Names } from "./rules.js";
|
|
8
|
+
// [function]
|
|
9
|
+
export function createLifecycle(bus, win) {
|
|
10
|
+
// began before the game's `load`: the loading screen is the page's to
|
|
11
|
+
// keep. `false` once `load` passed without a begin.
|
|
12
|
+
let begun = null;
|
|
13
|
+
let loaded = false;
|
|
14
|
+
let last = -1;
|
|
15
|
+
let ended = null;
|
|
16
|
+
let warnedLate = false;
|
|
17
|
+
let warnedValue = false;
|
|
18
|
+
const framed = Boolean(win && win.parent !== win);
|
|
19
|
+
const late = function (call) {
|
|
20
|
+
if (warnedLate || !framed)
|
|
21
|
+
return;
|
|
22
|
+
warnedLate = true;
|
|
23
|
+
log("warn", `${call} came after the page loaded, so Arcadible's loading screen was already down. Call initBegin() as your code starts, before anything you await or schedule for later.`);
|
|
24
|
+
};
|
|
25
|
+
const onLoad = function () {
|
|
26
|
+
if (loaded)
|
|
27
|
+
return;
|
|
28
|
+
loaded = true;
|
|
29
|
+
if (begun === null)
|
|
30
|
+
begun = false;
|
|
31
|
+
bus.emit(Names.Loaded, { begun });
|
|
32
|
+
};
|
|
33
|
+
const begin = function (call) {
|
|
34
|
+
if (begun !== null) {
|
|
35
|
+
if (!begun)
|
|
36
|
+
late(call);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (loaded) {
|
|
40
|
+
begun = false;
|
|
41
|
+
late(call);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
begun = true;
|
|
45
|
+
bus.emit(Names.Begin);
|
|
46
|
+
};
|
|
47
|
+
// [start]
|
|
48
|
+
// the game's own `load` is the frame's on the page: a game that began by
|
|
49
|
+
// then keeps the loading screen. A script that runs after it (added by
|
|
50
|
+
// code, late) reports at once.
|
|
51
|
+
if (win && typeof win.addEventListener === "function") {
|
|
52
|
+
if (win.document?.readyState === "complete")
|
|
53
|
+
onLoad();
|
|
54
|
+
else
|
|
55
|
+
win.addEventListener("load", onLoad, { once: true });
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
begin() {
|
|
59
|
+
begin("initBegin");
|
|
60
|
+
},
|
|
61
|
+
progress(fraction) {
|
|
62
|
+
const value = fractionOf(fraction);
|
|
63
|
+
if (value === null) {
|
|
64
|
+
if (!warnedValue) {
|
|
65
|
+
warnedValue = true;
|
|
66
|
+
log("warn", "initProgress takes a number from 0 to 1.");
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
begin("initProgress");
|
|
71
|
+
if (!begun || ended || value <= last)
|
|
72
|
+
return;
|
|
73
|
+
last = value;
|
|
74
|
+
bus.emit(Names.Progress, { fraction: value });
|
|
75
|
+
},
|
|
76
|
+
end() {
|
|
77
|
+
if (ended)
|
|
78
|
+
return ended;
|
|
79
|
+
begin("initEnd");
|
|
80
|
+
// a page without a loading screen, an older one, or none standing
|
|
81
|
+
// alone, answers `unsupported`: the game is visible already.
|
|
82
|
+
ended = begun
|
|
83
|
+
? bus.request(Names.End, {}, isObject, { timeout: END_TIMEOUT }).then(() => undefined, () => undefined)
|
|
84
|
+
: Promise.resolve();
|
|
85
|
+
return ended;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const Names: {
|
|
2
|
+
readonly Begin: "lifecycle.initBegin";
|
|
3
|
+
readonly Progress: "lifecycle.initProgress";
|
|
4
|
+
readonly Loaded: "lifecycle.loaded";
|
|
5
|
+
readonly End: "lifecycle.initEnd";
|
|
6
|
+
};
|
|
7
|
+
export declare const INIT_SINCE = "1.3.0";
|
|
8
|
+
export declare const END_TIMEOUT = 60000;
|
|
9
|
+
export declare function reportsInit(client: unknown): boolean;
|
|
10
|
+
export declare function fractionOf(value: unknown): number | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The game's initialization on the bus, shared by both ends: the messages'
|
|
2
|
+
// names, and the first SDK that sends them (DESIGN.md, section 7,
|
|
3
|
+
// Initialization). On the bus they're `lifecycle`'s; games call them on
|
|
4
|
+
// `arcadible` itself.
|
|
5
|
+
// [declare]
|
|
6
|
+
export const Names = {
|
|
7
|
+
// the game began its initialization: an event.
|
|
8
|
+
Begin: "lifecycle.initBegin",
|
|
9
|
+
// how far along it is, `{ fraction }`, from 0 to 1: an event.
|
|
10
|
+
Progress: "lifecycle.initProgress",
|
|
11
|
+
// the game's own `load`, `{ begun }`: whether it began before it. An
|
|
12
|
+
// event, sent once, so the page decides without a timer.
|
|
13
|
+
Loaded: "lifecycle.loaded",
|
|
14
|
+
// its first frame is drawn: a request, answered once the game is visible.
|
|
15
|
+
End: "lifecycle.initEnd",
|
|
16
|
+
};
|
|
17
|
+
// The first SDK that sends them. An older one can't, so the page takes its
|
|
18
|
+
// loading screen down at `load`, without waiting to hear.
|
|
19
|
+
export const INIT_SINCE = "1.3.0";
|
|
20
|
+
// How long `initEnd()` waits for the page to say the game is visible: past
|
|
21
|
+
// the loading screen's minimum and fade, and a device held the wrong way
|
|
22
|
+
// for a while. Past it, the game goes on as if visible.
|
|
23
|
+
export const END_TIMEOUT = 60_000;
|
|
24
|
+
// [function]
|
|
25
|
+
// a version's numbers, `1.3.0-beta.1` as [1, 3, 0], or null.
|
|
26
|
+
function numbers(version) {
|
|
27
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/u.exec(version);
|
|
28
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
29
|
+
}
|
|
30
|
+
// [function]
|
|
31
|
+
// Whether an SDK, by the version in its hello, can report its
|
|
32
|
+
// initialization: `INIT_SINCE` or later. Anything unreadable can't.
|
|
33
|
+
export function reportsInit(client) {
|
|
34
|
+
if (typeof client !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
const have = numbers(client);
|
|
37
|
+
const since = numbers(INIT_SINCE);
|
|
38
|
+
if (!have)
|
|
39
|
+
return false;
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (have[i] !== since[i])
|
|
42
|
+
return have[i] > since[i];
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// [function]
|
|
47
|
+
// a fraction as the page takes it: a finite number, clamped to 0 to 1, or
|
|
48
|
+
// null.
|
|
49
|
+
export function fractionOf(value) {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
51
|
+
return null;
|
|
52
|
+
return Math.min(1, Math.max(0, value));
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Module } from "../bus/server.js";
|
|
2
|
+
export type LifecycleHost = {
|
|
3
|
+
begin(): void;
|
|
4
|
+
progress(fraction: number): void;
|
|
5
|
+
loaded(begun: boolean): void;
|
|
6
|
+
end(): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare function lifecycleServer(host: LifecycleHost): () => Module;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The page's side of the game's initialization: it passes what the game
|
|
2
|
+
// reports to the page's loading screen, and answers `initEnd` once the page
|
|
3
|
+
// says the game is visible. A page without a loading screen leaves it out,
|
|
4
|
+
// so `initEnd` gets `unsupported`, which a game takes as visible at once.
|
|
5
|
+
// [import]
|
|
6
|
+
import { isObject } from "../bus/protocol.js";
|
|
7
|
+
import { fractionOf, Names } from "./rules.js";
|
|
8
|
+
// [function]
|
|
9
|
+
export function lifecycleServer(host) {
|
|
10
|
+
return () => ({
|
|
11
|
+
handlers: {
|
|
12
|
+
[Names.End]: {
|
|
13
|
+
parallel: true,
|
|
14
|
+
run: async () => {
|
|
15
|
+
await host.end();
|
|
16
|
+
return {};
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
events: {
|
|
21
|
+
[Names.Begin]: () => host.begin(),
|
|
22
|
+
[Names.Progress]: (data) => {
|
|
23
|
+
const fraction = isObject(data) ? fractionOf(data["fraction"]) : null;
|
|
24
|
+
if (fraction !== null)
|
|
25
|
+
host.progress(fraction);
|
|
26
|
+
},
|
|
27
|
+
[Names.Loaded]: (data) => {
|
|
28
|
+
host.loaded(isObject(data) && data["begun"] === true);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/saves/rules.d.ts
CHANGED
|
@@ -47,7 +47,6 @@ export declare function checkWrite(saves: ReadonlyMap<string, {
|
|
|
47
47
|
}>, name: unknown, data: unknown): ArcadibleError | null;
|
|
48
48
|
export declare function isSaveMeta(value: unknown): value is SaveMeta;
|
|
49
49
|
export declare function isSaveInfoOrNull(value: unknown): value is SaveInfo | null;
|
|
50
|
-
export declare function isSave(value: unknown): value is Save;
|
|
51
50
|
export declare function isSavesLoad(value: unknown): value is SavesLoad;
|
|
52
51
|
export declare function isSavesChange(value: unknown): value is SavesChange;
|
|
53
52
|
export declare function isRevisionOrNull(value: unknown): value is number | null;
|
package/dist/saves/rules.js
CHANGED
|
@@ -65,7 +65,7 @@ export function isSaveInfoOrNull(value) {
|
|
|
65
65
|
typeof value["savedAt"] === "string"));
|
|
66
66
|
}
|
|
67
67
|
// [function]
|
|
68
|
-
|
|
68
|
+
function isSave(value) {
|
|
69
69
|
return (isObject(value) &&
|
|
70
70
|
checkName(value["name"]) === null &&
|
|
71
71
|
typeof value["data"] === "string" &&
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.3.1";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED