@arcadible/sdk 1.5.0 → 1.5.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 +2 -4
- package/dist/client.js +17 -9
- package/dist/context/client.d.ts +2 -4
- package/dist/context/client.js +9 -23
- package/dist/facts.d.ts +11 -0
- package/dist/facts.js +33 -0
- package/dist/lifecycle/pause.d.ts +1 -3
- package/dist/lifecycle/pause.js +26 -50
- package/dist/listeners.d.ts +13 -0
- package/dist/listeners.js +48 -0
- package/dist/system/client.d.ts +2 -4
- package/dist/system/client.js +20 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,13 +68,11 @@ Off Arcadible, such as on your dev server, the package still works, with the sam
|
|
|
68
68
|
|
|
69
69
|
```js
|
|
70
70
|
arcadible.on("pause", async () => {
|
|
71
|
-
loop
|
|
72
|
-
audio.mute();
|
|
71
|
+
// … stop your game loop and timers, and silence your audio …
|
|
73
72
|
await arcadible.saves.write("progress", JSON.stringify(progress));
|
|
74
73
|
});
|
|
75
74
|
arcadible.on("resume", () => {
|
|
76
|
-
loop
|
|
77
|
-
audio.unmute();
|
|
75
|
+
// … start your game loop and timers again, and your audio …
|
|
78
76
|
});
|
|
79
77
|
```
|
|
80
78
|
|
package/dist/client.js
CHANGED
|
@@ -33,28 +33,36 @@ export function createClient(options = {}) {
|
|
|
33
33
|
requestTimeout: options.requestTimeout,
|
|
34
34
|
standalone: () => serve({ ...STANDALONE, backend: backend() }),
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
// each module's game side, made on the bus as the client starts, before
|
|
37
|
+
// anything arrives.
|
|
38
|
+
const saves = createSaves(bus, {
|
|
37
39
|
timeout: options.requestTimeout ?? REQUEST_TIMEOUT,
|
|
38
40
|
});
|
|
39
|
-
let settled = null;
|
|
40
41
|
const system = createSystemClient(bus);
|
|
41
42
|
const context = createContextClient(bus);
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const lifecycle = createLifecycle(bus, win);
|
|
44
|
+
const pause = createPause(bus);
|
|
45
|
+
// the ones `ready` waits on, or tells: lifecycle and pause do neither.
|
|
46
|
+
const parts = [saves, system, context];
|
|
47
|
+
let settled = null;
|
|
48
|
+
// ready once welcomed, and each module's first messages have arrived;
|
|
49
|
+
// then each is told, so its facts are known, or null.
|
|
50
|
+
const ready = Promise.all([
|
|
51
|
+
bus.welcomed,
|
|
52
|
+
...parts.map((part) => part.opened),
|
|
53
|
+
]).then(([welcome]) => {
|
|
54
|
+
for (const part of parts)
|
|
55
|
+
part.settle?.();
|
|
46
56
|
settled = { build: buildOf(welcome.build) };
|
|
47
57
|
return settled;
|
|
48
58
|
});
|
|
49
|
-
const lifecycle = createLifecycle(bus, win);
|
|
50
|
-
const pause = createPause(bus);
|
|
51
59
|
return {
|
|
52
60
|
version: VERSION,
|
|
53
61
|
ready,
|
|
54
62
|
get build() {
|
|
55
63
|
return settled ? settled.build : undefined;
|
|
56
64
|
},
|
|
57
|
-
saves,
|
|
65
|
+
saves: saves.saves,
|
|
58
66
|
get system() {
|
|
59
67
|
return system.value;
|
|
60
68
|
},
|
package/dist/context/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Bus } from "../bus/client.js";
|
|
2
|
+
import { type Fact } from "../facts.js";
|
|
2
3
|
/** How this run started, fixed for the run. */
|
|
3
4
|
export interface Context {
|
|
4
5
|
readonly launch: {
|
|
@@ -6,7 +7,4 @@ export interface Context {
|
|
|
6
7
|
readonly at: string;
|
|
7
8
|
};
|
|
8
9
|
}
|
|
9
|
-
export declare function createContextClient(bus: Bus):
|
|
10
|
-
settle(): void;
|
|
11
|
-
readonly value: Context | null | undefined;
|
|
12
|
-
};
|
|
10
|
+
export declare function createContextClient(bus: Bus): Fact<Context>;
|
package/dist/context/client.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
// The game's side of `context`: `arcadible.context`, how this run started,
|
|
2
2
|
// fixed for the run. `undefined` before `ready`, and `null` where no page
|
|
3
3
|
// says (standing alone, or an older page).
|
|
4
|
-
import {
|
|
4
|
+
import { fact } from "../facts.js";
|
|
5
5
|
import { contextOf, Names } from "./rules.js";
|
|
6
6
|
// [function]
|
|
7
7
|
export function createContextClient(bus) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!next) {
|
|
16
|
-
log("warn", "ignored a malformed context from the play page.");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
context = Object.freeze({ launch: Object.freeze({ ...next.launch }) });
|
|
20
|
-
});
|
|
21
|
-
return {
|
|
22
|
-
settle() {
|
|
23
|
-
settled = true;
|
|
24
|
-
},
|
|
25
|
-
get value() {
|
|
26
|
-
if (!settled)
|
|
27
|
-
return undefined;
|
|
28
|
-
return context;
|
|
8
|
+
return fact(bus, Names.State, {
|
|
9
|
+
fixed: true,
|
|
10
|
+
parse(data) {
|
|
11
|
+
const context = contextOf(data);
|
|
12
|
+
return context
|
|
13
|
+
? Object.freeze({ launch: Object.freeze({ ...context.launch }) })
|
|
14
|
+
: null;
|
|
29
15
|
},
|
|
30
|
-
};
|
|
16
|
+
});
|
|
31
17
|
}
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Bus } from "./bus/client.js";
|
|
2
|
+
export type Fact<T> = {
|
|
3
|
+
settle(): void;
|
|
4
|
+
readonly value: T | null | undefined;
|
|
5
|
+
};
|
|
6
|
+
export type FactOptions<T> = {
|
|
7
|
+
parse(value: unknown): T | null;
|
|
8
|
+
fixed?: boolean;
|
|
9
|
+
changed?(before: T, next: T): void;
|
|
10
|
+
};
|
|
11
|
+
export declare function fact<T>(bus: Bus, name: string, options: FactOptions<T>): Fact<T>;
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A fact the page sends as its module opens, such as `system`'s state or
|
|
2
|
+
// `context`: `undefined` before `ready`, the fact from then on, or `null`
|
|
3
|
+
// where no page sent one (standing alone, or an older page). Only what
|
|
4
|
+
// `parse` keeps is kept, so the host is trusted with nothing; a fixed fact
|
|
5
|
+
// takes its first and nothing after `ready`, and a changing one tells
|
|
6
|
+
// `changed` what came after it.
|
|
7
|
+
import { isObject, log } from "./bus/protocol.js";
|
|
8
|
+
// [function]
|
|
9
|
+
export function fact(bus, name, options) {
|
|
10
|
+
let value = null;
|
|
11
|
+
let settled = false;
|
|
12
|
+
bus.on(name, isObject, (data) => {
|
|
13
|
+
if (options.fixed && (value !== null || settled))
|
|
14
|
+
return;
|
|
15
|
+
const next = options.parse(data);
|
|
16
|
+
if (next === null) {
|
|
17
|
+
log("warn", `ignored a malformed ${name} from the play page.`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const before = value;
|
|
21
|
+
value = next;
|
|
22
|
+
if (before !== null && settled)
|
|
23
|
+
options.changed?.(before, next);
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
settle() {
|
|
27
|
+
settled = true;
|
|
28
|
+
},
|
|
29
|
+
get value() {
|
|
30
|
+
return settled ? value : undefined;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Bus } from "../bus/client.js";
|
|
2
|
-
export type PauseListener = () => void | Promise<void>;
|
|
3
|
-
export type ResumeListener = () => void;
|
|
4
2
|
export type Pause = {
|
|
5
|
-
on(name:
|
|
3
|
+
on(name: unknown, listener: unknown): () => void;
|
|
6
4
|
};
|
|
7
5
|
export declare function createPause(bus: Bus): Pause;
|
package/dist/lifecycle/pause.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
// why; this side keeps that from the moment it connects, so a game that
|
|
4
4
|
// subscribes while held still gets its pause, and tells the page once its
|
|
5
5
|
// pause listeners have settled, which the page waits on only as it closes.
|
|
6
|
-
import {
|
|
6
|
+
import { isObject, log } from "../bus/protocol.js";
|
|
7
|
+
import { listeners } from "../listeners.js";
|
|
7
8
|
import { Names } from "./rules.js";
|
|
8
|
-
// the events a game can listen for on `arcadible`: a closed list, so a typo
|
|
9
|
-
// throws rather than never firing.
|
|
10
|
-
const EVENTS = ["pause", "resume"];
|
|
11
9
|
// [function]
|
|
12
10
|
function isHeld(value) {
|
|
13
11
|
return (isObject(value) &&
|
|
@@ -22,21 +20,14 @@ export function createPause(bus) {
|
|
|
22
20
|
// whether the game heard this hold's pause: a resume goes only to a game
|
|
23
21
|
// that did, so it never resumes from a pause it wasn't told of.
|
|
24
22
|
let heard = false;
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
// runs pause listeners,
|
|
28
|
-
//
|
|
29
|
-
const pause = function (
|
|
30
|
-
|
|
23
|
+
// the events a game can listen for on `arcadible`.
|
|
24
|
+
const on = listeners("arcadible", ["pause", "resume"]);
|
|
25
|
+
// runs pause listeners, and tells the page once they've all settled, for
|
|
26
|
+
// pause `count`.
|
|
27
|
+
const pause = function (run, count) {
|
|
28
|
+
const runs = run().map((result) => Promise.resolve(result));
|
|
29
|
+
if (runs.length > 0)
|
|
31
30
|
heard = true;
|
|
32
|
-
const runs = listeners.map((listener) => {
|
|
33
|
-
try {
|
|
34
|
-
return Promise.resolve(listener());
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
return Promise.reject(err);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
31
|
void Promise.allSettled(runs).then((results) => {
|
|
41
32
|
for (const result of results) {
|
|
42
33
|
if (result.status === "rejected") {
|
|
@@ -46,45 +37,23 @@ export function createPause(bus) {
|
|
|
46
37
|
bus.emit(Names.Paused, { n: count });
|
|
47
38
|
});
|
|
48
39
|
};
|
|
49
|
-
const resume = function () {
|
|
50
|
-
for (const listener of [...resumes]) {
|
|
51
|
-
try {
|
|
52
|
-
listener();
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
log("error", "a resume listener failed.", err);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
40
|
bus.on(Names.Held, isHeld, (data) => {
|
|
60
41
|
const was = held;
|
|
61
42
|
held = data.held;
|
|
62
43
|
n = data.n;
|
|
63
44
|
if (held && !was)
|
|
64
|
-
pause(
|
|
45
|
+
pause(() => on.call("pause"), n);
|
|
65
46
|
else if (!held && was) {
|
|
66
47
|
if (heard)
|
|
67
|
-
resume
|
|
48
|
+
on.call("resume");
|
|
68
49
|
heard = false;
|
|
69
50
|
}
|
|
70
51
|
});
|
|
71
52
|
return {
|
|
72
53
|
on(name, listener) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof listener !== "function") {
|
|
77
|
-
throw new ArcadibleError(Codes.Invalid, `arcadible.on("${name}") takes a function.`);
|
|
78
|
-
}
|
|
79
|
-
if (name === "resume") {
|
|
80
|
-
const fn = listener;
|
|
81
|
-
resumes.add(fn);
|
|
82
|
-
return () => {
|
|
83
|
-
resumes.delete(fn);
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const fn = listener;
|
|
87
|
-
pauses.add(fn);
|
|
54
|
+
const off = on.add(name, listener);
|
|
55
|
+
if (name !== "pause")
|
|
56
|
+
return off;
|
|
88
57
|
if (!told) {
|
|
89
58
|
told = true;
|
|
90
59
|
bus.emit(Names.Listening);
|
|
@@ -94,13 +63,20 @@ export function createPause(bus) {
|
|
|
94
63
|
if (held) {
|
|
95
64
|
const count = n;
|
|
96
65
|
queueMicrotask(() => {
|
|
97
|
-
if (held && n === count &&
|
|
98
|
-
pause(
|
|
66
|
+
if (held && n === count && on.has("pause", listener)) {
|
|
67
|
+
pause(() => {
|
|
68
|
+
try {
|
|
69
|
+
return [listener()];
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
log("error", "a pause listener failed.", err);
|
|
73
|
+
return [undefined];
|
|
74
|
+
}
|
|
75
|
+
}, count);
|
|
76
|
+
}
|
|
99
77
|
});
|
|
100
78
|
}
|
|
101
|
-
return
|
|
102
|
-
pauses.delete(fn);
|
|
103
|
-
};
|
|
79
|
+
return off;
|
|
104
80
|
},
|
|
105
81
|
};
|
|
106
82
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Listener = (...args: never[]) => unknown;
|
|
2
|
+
export type Listeners<N extends string> = {
|
|
3
|
+
add(name: unknown, listener: unknown): () => void;
|
|
4
|
+
of(name: N): Listener[];
|
|
5
|
+
has(name: N, listener: unknown): boolean;
|
|
6
|
+
call(name: N, ...args: unknown[]): unknown[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The listeners for `names` on the object a game calls as `where`
|
|
10
|
+
* (`"arcadible"`, `"arcadible.system"`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function listeners<N extends string>(where: string, names: readonly N[]): Listeners<N>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The listeners behind every `on` a game calls (`arcadible.on`,
|
|
2
|
+
// `arcadible.system.on`): a closed list of names, so a typo throws rather
|
|
3
|
+
// than never firing; a listener that must be a function; the function that
|
|
4
|
+
// unsubscribes; and each listener run on its own, so one that throws stops
|
|
5
|
+
// none of the others. One shape for each, with the same messages.
|
|
6
|
+
// [import]
|
|
7
|
+
import { ArcadibleError, Codes, log } from "./bus/protocol.js";
|
|
8
|
+
// [function]
|
|
9
|
+
/**
|
|
10
|
+
* The listeners for `names` on the object a game calls as `where`
|
|
11
|
+
* (`"arcadible"`, `"arcadible.system"`).
|
|
12
|
+
*/
|
|
13
|
+
export function listeners(where, names) {
|
|
14
|
+
const sets = new Map(names.map((name) => [name, new Set()]));
|
|
15
|
+
const list = names.map((name) => JSON.stringify(name)).join(" or ");
|
|
16
|
+
const of = (name) => [...(sets.get(name) ?? [])];
|
|
17
|
+
return {
|
|
18
|
+
add(name, listener) {
|
|
19
|
+
const set = sets.get(name);
|
|
20
|
+
if (!set) {
|
|
21
|
+
throw new ArcadibleError(Codes.Invalid, `${where}.on takes ${list}, not ${JSON.stringify(name)}.`);
|
|
22
|
+
}
|
|
23
|
+
if (typeof listener !== "function") {
|
|
24
|
+
throw new ArcadibleError(Codes.Invalid, `${where}.on(${JSON.stringify(name)}) takes a function.`);
|
|
25
|
+
}
|
|
26
|
+
const fn = listener;
|
|
27
|
+
set.add(fn);
|
|
28
|
+
return () => {
|
|
29
|
+
set.delete(fn);
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
of,
|
|
33
|
+
has(name, listener) {
|
|
34
|
+
return sets.get(name)?.has(listener) ?? false;
|
|
35
|
+
},
|
|
36
|
+
call(name, ...args) {
|
|
37
|
+
return of(name).map((fn) => {
|
|
38
|
+
try {
|
|
39
|
+
return fn(...args);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log("error", `a ${name} listener failed.`, err);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/system/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Bus } from "../bus/client.js";
|
|
2
|
+
import { type Fact } from "../facts.js";
|
|
2
3
|
/** Arcadible's state for the player, which changes while the game runs. */
|
|
3
4
|
export interface System {
|
|
4
5
|
/**
|
|
@@ -14,7 +15,4 @@ export interface System {
|
|
|
14
15
|
*/
|
|
15
16
|
on(name: "change", listener: (system: System, changed: readonly string[]) => void): () => void;
|
|
16
17
|
}
|
|
17
|
-
export declare function createSystemClient(bus: Bus):
|
|
18
|
-
settle(): void;
|
|
19
|
-
readonly value: System | null | undefined;
|
|
20
|
-
};
|
|
18
|
+
export declare function createSystemClient(bus: Bus): Fact<System>;
|
package/dist/system/client.js
CHANGED
|
@@ -2,61 +2,36 @@
|
|
|
2
2
|
// player, read at any time, with one `change` event naming what changed.
|
|
3
3
|
// `undefined` before `ready`, and `null` where no page says (standing
|
|
4
4
|
// alone, or an older page).
|
|
5
|
-
import {
|
|
5
|
+
import { fact } from "../facts.js";
|
|
6
|
+
import { listeners } from "../listeners.js";
|
|
6
7
|
import { Names, stateOf } from "./rules.js";
|
|
7
8
|
// [function]
|
|
8
9
|
export function createSystemClient(bus) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// the events a game can listen for on `arcadible.system`.
|
|
11
|
+
const on = listeners("arcadible.system", ["change"]);
|
|
12
|
+
const kept = fact(bus, Names.State, {
|
|
13
|
+
parse: stateOf,
|
|
14
|
+
changed(before, next) {
|
|
15
|
+
const changed = Object.keys(next).filter((key) => next[key] !== before[key]);
|
|
16
|
+
if (changed.length > 0)
|
|
17
|
+
on.call("change", system, changed);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
// one object for the run: what a game holds, and what `change` passes,
|
|
21
|
+
// reads the state as it is now.
|
|
12
22
|
const system = {
|
|
13
23
|
get language() {
|
|
14
|
-
return
|
|
15
|
-
},
|
|
16
|
-
on(name, listener) {
|
|
17
|
-
if (name !== "change") {
|
|
18
|
-
throw new ArcadibleError(Codes.Invalid, `arcadible.system.on takes "change", not ${JSON.stringify(name)}.`);
|
|
19
|
-
}
|
|
20
|
-
if (typeof listener !== "function") {
|
|
21
|
-
throw new ArcadibleError(Codes.Invalid, 'arcadible.system.on("change") takes a function.');
|
|
22
|
-
}
|
|
23
|
-
const fn = listener;
|
|
24
|
-
listeners.add(fn);
|
|
25
|
-
return () => {
|
|
26
|
-
listeners.delete(fn);
|
|
27
|
-
};
|
|
24
|
+
return kept.value.language;
|
|
28
25
|
},
|
|
26
|
+
on: (name, listener) => on.add(name, listener),
|
|
29
27
|
};
|
|
30
|
-
bus.on(Names.State, isObject, (data) => {
|
|
31
|
-
const next = stateOf(data);
|
|
32
|
-
if (!next) {
|
|
33
|
-
log("warn", "ignored a malformed system state from the play page.");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const before = state;
|
|
37
|
-
state = next;
|
|
38
|
-
if (!before || !settled)
|
|
39
|
-
return;
|
|
40
|
-
const changed = Object.keys(next).filter((key) => next[key] !== before[key]);
|
|
41
|
-
if (changed.length === 0)
|
|
42
|
-
return;
|
|
43
|
-
for (const listener of [...listeners]) {
|
|
44
|
-
try {
|
|
45
|
-
listener(system, changed);
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
log("error", "a system change listener failed.", err);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
28
|
return {
|
|
53
|
-
settle()
|
|
54
|
-
settled = true;
|
|
55
|
-
},
|
|
29
|
+
settle: () => kept.settle(),
|
|
56
30
|
get value() {
|
|
57
|
-
|
|
31
|
+
const value = kept.value;
|
|
32
|
+
if (value === undefined)
|
|
58
33
|
return undefined;
|
|
59
|
-
return
|
|
34
|
+
return value === null ? null : system;
|
|
60
35
|
},
|
|
61
36
|
};
|
|
62
37
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.5.
|
|
1
|
+
export declare const VERSION = "1.5.1";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED