@arcadible/sdk 1.1.0 → 1.2.0
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 +4 -4
- package/dist/bus/client.d.ts +7 -3
- package/dist/bus/client.js +107 -15
- package/dist/bus/protocol.d.ts +18 -0
- package/dist/bus/protocol.js +45 -4
- package/dist/bus/server.d.ts +2 -1
- package/dist/bus/server.js +50 -27
- package/dist/client.d.ts +14 -0
- package/dist/client.js +11 -5
- package/dist/host.d.ts +2 -0
- package/dist/host.js +1 -0
- package/dist/saves/backend/indexeddb.d.ts +0 -1
- package/dist/saves/backend/indexeddb.js +53 -28
- package/dist/saves/backend/listeners.js +2 -1
- package/dist/saves/backend/memory.d.ts +2 -2
- package/dist/saves/backend/memory.js +4 -3
- package/dist/saves/backend/types.d.ts +2 -2
- package/dist/saves/client.d.ts +20 -2
- package/dist/saves/client.js +146 -87
- package/dist/saves/rules.d.ts +21 -2
- package/dist/saves/rules.js +46 -1
- package/dist/saves/server.d.ts +2 -0
- package/dist/saves/server.js +29 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<br>
|
|
21
21
|
|
|
22
|
-
Each
|
|
22
|
+
Each build of a game on [Arcadible](https://arcadible.com) has its own origin, so its browser storage starts empty with every update. Saves made through the Arcadible SDK are kept by the play page instead, on the player's device, so they carry over. The SDK is optional, and saves are all it does.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ Each version of a game on [Arcadible](https://arcadible.com) has its own origin,
|
|
|
27
27
|
npm install @arcadible/sdk
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Use the package in a game with a build step, such as a Vite project. A game without one includes the hosted script instead, which Arcadible serves from every build's own origin, as `arc serve` does locally:
|
|
31
31
|
|
|
32
32
|
```html
|
|
33
33
|
<script src="/arcadible/sdk/v1.js"></script>
|
|
@@ -46,9 +46,9 @@ const progress = saved ? JSON.parse(saved) : { level: 1 };
|
|
|
46
46
|
await arcadible.saves.write("progress", JSON.stringify(progress));
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
A save is a name holding one string. `ready` settles once the game's saves have loaded, and never fails; from then on, `readSync`
|
|
49
|
+
A save is a name holding one string. `ready` settles once the game's saves have loaded, and never fails; from then on, `readSync`, `listSync`, and `infoSync` answer from memory. 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.
|
|
50
50
|
|
|
51
|
-
Off Arcadible, such as on your dev server, the package works on its own, with the same rules and limits: saves are kept on the page's own origin, and `arcadible.build` is `null`.
|
|
51
|
+
Off Arcadible, such as on your dev server, the package works on its own, with the same rules and limits: saves are kept on the page's own origin, `arcadible.saves.where` is `"build"`, and `arcadible.build` is `null`.
|
|
52
52
|
|
|
53
53
|
## Learn more
|
|
54
54
|
|
package/dist/bus/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Welcome } from "./protocol.js";
|
|
1
|
+
import { type Shape, type Welcome } from "./protocol.js";
|
|
2
2
|
import type { Server } from "./server.js";
|
|
3
3
|
export type BusOptions = {
|
|
4
4
|
client: string;
|
|
@@ -8,9 +8,13 @@ export type BusOptions = {
|
|
|
8
8
|
requestTimeout?: number | undefined;
|
|
9
9
|
standalone: () => Server;
|
|
10
10
|
};
|
|
11
|
+
export type RequestOptions<T> = {
|
|
12
|
+
late?: (result: T) => void;
|
|
13
|
+
};
|
|
11
14
|
export type Bus = {
|
|
12
15
|
readonly welcomed: Promise<Welcome>;
|
|
13
|
-
request(name: string, params?:
|
|
14
|
-
on(name: string, listener: (data:
|
|
16
|
+
request<T>(name: string, params: object | undefined, shape: Shape<T>, options?: RequestOptions<T>): Promise<T>;
|
|
17
|
+
on<T>(name: string, shape: Shape<T>, listener: (data: T) => void): () => void;
|
|
15
18
|
};
|
|
19
|
+
export declare function mayHaveLanded(err: unknown): boolean;
|
|
16
20
|
export declare function openBus(options: BusOptions): Bus;
|
package/dist/bus/client.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
// The game's end of the bus: it says hello to the play page, and stands
|
|
2
2
|
// alone when no page answers, running the page's end itself, in its own
|
|
3
|
-
// page, so the rules are the same.
|
|
3
|
+
// page, so the rules are the same. It takes nothing the page sends on trust:
|
|
4
|
+
// every call settles, with what its request expects or a code.
|
|
4
5
|
// [import]
|
|
5
|
-
import { ArcadibleError, BUS_VERSION, Codes, HANDSHAKE_TIMEOUT, isHostOrigin, isMessage, isWelcome, REQUEST_TIMEOUT, } from "./protocol.js";
|
|
6
|
+
import { ArcadibleError, BUS_VERSION, Codes, HANDSHAKE_TIMEOUT, isErrorBody, isHostOrigin, isMessage, isWelcome, log, REQUEST_TIMEOUT, } from "./protocol.js";
|
|
7
|
+
// [declare]
|
|
8
|
+
// How many calls that timed out keep waiting for a late answer; past it,
|
|
9
|
+
// the oldest stop.
|
|
10
|
+
const MAX_LATE = 64;
|
|
11
|
+
// failures after which the page may still have done what was asked: a
|
|
12
|
+
// `timeout`, or an answer the game couldn't read.
|
|
13
|
+
const uncertain = new WeakSet();
|
|
14
|
+
// [function]
|
|
15
|
+
// Whether the page may have done what a failed call asked, so a game's
|
|
16
|
+
// memory copy keeps what it showed.
|
|
17
|
+
export function mayHaveLanded(err) {
|
|
18
|
+
return err instanceof ArcadibleError && uncertain.has(err);
|
|
19
|
+
}
|
|
20
|
+
// [function]
|
|
21
|
+
function unsure(code, message) {
|
|
22
|
+
const err = new ArcadibleError(code, message);
|
|
23
|
+
uncertain.add(err);
|
|
24
|
+
return err;
|
|
25
|
+
}
|
|
6
26
|
// [function]
|
|
7
27
|
export function openBus(options) {
|
|
8
28
|
const win = options.window ?? (typeof window === "undefined" ? undefined : window);
|
|
@@ -13,6 +33,7 @@ export function openBus(options) {
|
|
|
13
33
|
let nextId = 1;
|
|
14
34
|
let handshakeTimer;
|
|
15
35
|
const pending = new Map();
|
|
36
|
+
const late = new Map();
|
|
16
37
|
const listeners = new Map();
|
|
17
38
|
let welcome = () => undefined;
|
|
18
39
|
const welcomed = new Promise((resolve) => {
|
|
@@ -23,22 +44,62 @@ export function openBus(options) {
|
|
|
23
44
|
const message = event.data;
|
|
24
45
|
if (!isMessage(message))
|
|
25
46
|
return;
|
|
26
|
-
|
|
47
|
+
try {
|
|
48
|
+
handle(message);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
log("error", "a message from the play page failed.", err);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// [function]
|
|
55
|
+
// a call's answer: settled before anything else is read, so whatever the
|
|
56
|
+
// page sent, the call ends.
|
|
57
|
+
const settle = function (call, message) {
|
|
58
|
+
const error = message["error"];
|
|
59
|
+
if (error !== undefined) {
|
|
60
|
+
if (!isErrorBody(error)) {
|
|
61
|
+
call.reject(unsure(Codes.Unavailable, "The play page's answer was malformed."));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const text = error.message;
|
|
65
|
+
call.reject(new ArcadibleError(error.code, typeof text === "string" ? text : "That failed. Try again."));
|
|
66
|
+
}
|
|
67
|
+
else if (call.shape(message["result"])) {
|
|
68
|
+
call.resolve(message["result"]);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
call.reject(unsure(Codes.Unavailable, "The play page's answer was malformed."));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// [function]
|
|
75
|
+
const answerLate = function (id, message) {
|
|
76
|
+
const call = late.get(id);
|
|
77
|
+
if (!call)
|
|
78
|
+
return;
|
|
79
|
+
late.delete(id);
|
|
80
|
+
const result = message["result"];
|
|
81
|
+
if (message["error"] === undefined && call.shape(result)) {
|
|
82
|
+
call.late(result);
|
|
83
|
+
}
|
|
27
84
|
};
|
|
28
85
|
// [function]
|
|
29
86
|
const handle = function (message) {
|
|
30
87
|
if (message.kind === "response") {
|
|
31
88
|
const call = pending.get(message.id);
|
|
32
|
-
if (!call)
|
|
89
|
+
if (!call) {
|
|
90
|
+
answerLate(message.id, message);
|
|
33
91
|
return;
|
|
92
|
+
}
|
|
34
93
|
pending.delete(message.id);
|
|
35
94
|
clearTimeout(call.timer);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
call.reject(new ArcadibleError(code, text));
|
|
95
|
+
try {
|
|
96
|
+
settle(call, message);
|
|
39
97
|
}
|
|
40
|
-
|
|
41
|
-
call
|
|
98
|
+
catch (err) {
|
|
99
|
+
// `settle` rejects before it could throw; this only keeps a call
|
|
100
|
+
// from hanging if that ever changes.
|
|
101
|
+
call.reject(unsure(Codes.Unavailable, "The play page's answer was malformed."));
|
|
102
|
+
log("error", `${call.name}'s answer couldn't be read.`, err);
|
|
42
103
|
}
|
|
43
104
|
}
|
|
44
105
|
else if (message.kind === "event") {
|
|
@@ -71,6 +132,9 @@ export function openBus(options) {
|
|
|
71
132
|
return;
|
|
72
133
|
state = "standalone";
|
|
73
134
|
win?.removeEventListener("message", onWindowMessage);
|
|
135
|
+
if (win && win.parent !== win) {
|
|
136
|
+
log("warn", 'no play page answered within a second, so this game keeps its saves on its own origin, where only this build reads them (saves.where is "build"); reloading usually fixes it.');
|
|
137
|
+
}
|
|
74
138
|
const channel = new MessageChannel();
|
|
75
139
|
attach(channel.port1, options.standalone().connect(channel.port2));
|
|
76
140
|
};
|
|
@@ -101,7 +165,7 @@ export function openBus(options) {
|
|
|
101
165
|
}
|
|
102
166
|
return {
|
|
103
167
|
welcomed,
|
|
104
|
-
async request(name, params) {
|
|
168
|
+
async request(name, params, shape, requestOptions = {}) {
|
|
105
169
|
await welcomed;
|
|
106
170
|
// welcomed, so there's a channel: the play page's, or this page's own.
|
|
107
171
|
const link = port;
|
|
@@ -111,22 +175,50 @@ export function openBus(options) {
|
|
|
111
175
|
const id = nextId++;
|
|
112
176
|
return new Promise((resolve, reject) => {
|
|
113
177
|
const timer = setTimeout(() => {
|
|
178
|
+
const call = pending.get(id);
|
|
114
179
|
pending.delete(id);
|
|
115
|
-
|
|
180
|
+
if (call?.late) {
|
|
181
|
+
late.set(id, { shape: call.shape, late: call.late });
|
|
182
|
+
if (late.size > MAX_LATE) {
|
|
183
|
+
late.delete(late.keys().next().value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
reject(unsure(Codes.Timeout, "The play page took too long to answer. Try again."));
|
|
116
187
|
}, requestTimeout);
|
|
117
|
-
pending.set(id, {
|
|
188
|
+
pending.set(id, {
|
|
189
|
+
name,
|
|
190
|
+
resolve: resolve,
|
|
191
|
+
reject,
|
|
192
|
+
timer,
|
|
193
|
+
shape,
|
|
194
|
+
late: requestOptions.late,
|
|
195
|
+
});
|
|
118
196
|
link.postMessage({ kind: "request", id, name, params });
|
|
119
197
|
});
|
|
120
198
|
},
|
|
121
|
-
on(name, listener) {
|
|
199
|
+
on(name, shape, listener) {
|
|
200
|
+
// each listener on its own: one that throws, or data it can't take,
|
|
201
|
+
// doesn't stop the others.
|
|
202
|
+
const checked = function (data) {
|
|
203
|
+
if (!shape(data)) {
|
|
204
|
+
log("warn", `ignored a malformed ${name} from the play page.`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
listener(data);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
log("error", `a ${name} listener failed.`, err);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
122
214
|
let set = listeners.get(name);
|
|
123
215
|
if (!set) {
|
|
124
216
|
set = new Set();
|
|
125
217
|
listeners.set(name, set);
|
|
126
218
|
}
|
|
127
|
-
set.add(
|
|
219
|
+
set.add(checked);
|
|
128
220
|
return () => {
|
|
129
|
-
set.delete(
|
|
221
|
+
set.delete(checked);
|
|
130
222
|
};
|
|
131
223
|
},
|
|
132
224
|
};
|
package/dist/bus/protocol.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export declare const Codes: {
|
|
|
16
16
|
readonly Unsupported: "unsupported";
|
|
17
17
|
};
|
|
18
18
|
export type Code = (typeof Codes)[keyof typeof Codes];
|
|
19
|
+
/**
|
|
20
|
+
* The build that's running, from the page's own lookup: its version, from
|
|
21
|
+
* `arcadible.toml` or `package.json`, and its channel (`production`,
|
|
22
|
+
* `staging`, or `local` under `arc serve`). `null` standing alone, or when
|
|
23
|
+
* the page doesn't say.
|
|
24
|
+
*/
|
|
19
25
|
export type Build = {
|
|
20
26
|
version: string;
|
|
21
27
|
channel: string;
|
|
@@ -30,6 +36,7 @@ export type Welcome = {
|
|
|
30
36
|
v: number;
|
|
31
37
|
build: Build | null;
|
|
32
38
|
};
|
|
39
|
+
export type Shape<T> = (value: unknown) => value is T;
|
|
33
40
|
export type ErrorBody = {
|
|
34
41
|
code: string;
|
|
35
42
|
message: string;
|
|
@@ -55,13 +62,24 @@ type BusEvent = {
|
|
|
55
62
|
data?: unknown;
|
|
56
63
|
};
|
|
57
64
|
export type Message = Request | Response | BusEvent;
|
|
65
|
+
/** A failed call. Match on `code`; the message is for people. */
|
|
58
66
|
export declare class ArcadibleError extends Error {
|
|
59
67
|
readonly code: Code;
|
|
60
68
|
constructor(code: Code, message: string);
|
|
61
69
|
toJSON(): ErrorBody;
|
|
62
70
|
}
|
|
71
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
72
|
+
export declare function isString(value: unknown): value is string;
|
|
73
|
+
export declare function isNumber(value: unknown): value is number;
|
|
74
|
+
export declare function isStringOrNull(value: unknown): value is string | null;
|
|
75
|
+
export declare function isStringArray(value: unknown): value is string[];
|
|
76
|
+
export declare function isErrorBody(value: unknown): value is {
|
|
77
|
+
code: string;
|
|
78
|
+
};
|
|
79
|
+
export declare function log(level: "warn" | "error", sentence: string, ...detail: unknown[]): void;
|
|
63
80
|
export declare function isHello(value: unknown): value is Hello;
|
|
64
81
|
export declare function isWelcome(value: unknown): value is Welcome;
|
|
82
|
+
export declare function buildOf(value: unknown): Build | null;
|
|
65
83
|
export declare function isMessage(value: unknown): value is Message;
|
|
66
84
|
export declare function utf8Size(text: string): number;
|
|
67
85
|
export declare function messageSize(value: unknown, limit?: number): number;
|
package/dist/bus/protocol.js
CHANGED
|
@@ -26,8 +26,10 @@ export function isHostOrigin(origin, origins) {
|
|
|
26
26
|
// The most requests one game can have waiting on the page; past it, a
|
|
27
27
|
// request fails with `busy`.
|
|
28
28
|
export const MAX_IN_FLIGHT = 64;
|
|
29
|
-
//
|
|
30
|
-
// save, and the rest of the
|
|
29
|
+
// The largest message either side sends: room for a whole 1 MB save, or
|
|
30
|
+
// every save at once (a game's are 1 MB together), and the rest of the
|
|
31
|
+
// message. The page refuses a larger request, and never sends a larger
|
|
32
|
+
// answer or event (DESIGN.md, section 4).
|
|
31
33
|
export const MAX_MESSAGE = 1024 * 1024 + 64 * 1024;
|
|
32
34
|
// [declare]
|
|
33
35
|
// Every code a call can fail with. Worth retrying: `busy`, `timeout`, and
|
|
@@ -46,7 +48,7 @@ export const Codes = {
|
|
|
46
48
|
Unsupported: "unsupported",
|
|
47
49
|
};
|
|
48
50
|
// [class]
|
|
49
|
-
|
|
51
|
+
/** A failed call. Match on `code`; the message is for people. */
|
|
50
52
|
export class ArcadibleError extends Error {
|
|
51
53
|
// [field]
|
|
52
54
|
code;
|
|
@@ -62,10 +64,38 @@ export class ArcadibleError extends Error {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
// [function]
|
|
65
|
-
function isObject(value) {
|
|
67
|
+
export function isObject(value) {
|
|
66
68
|
return typeof value === "object" && value !== null;
|
|
67
69
|
}
|
|
68
70
|
// [function]
|
|
71
|
+
export function isString(value) {
|
|
72
|
+
return typeof value === "string";
|
|
73
|
+
}
|
|
74
|
+
// [function]
|
|
75
|
+
export function isNumber(value) {
|
|
76
|
+
return typeof value === "number";
|
|
77
|
+
}
|
|
78
|
+
// [function]
|
|
79
|
+
export function isStringOrNull(value) {
|
|
80
|
+
return value === null || typeof value === "string";
|
|
81
|
+
}
|
|
82
|
+
// [function]
|
|
83
|
+
export function isStringArray(value) {
|
|
84
|
+
return Array.isArray(value) && value.every(isString);
|
|
85
|
+
}
|
|
86
|
+
// [function]
|
|
87
|
+
// What an error is sent as: a string `code`, and a message for people.
|
|
88
|
+
export function isErrorBody(value) {
|
|
89
|
+
return isObject(value) && typeof value["code"] === "string";
|
|
90
|
+
}
|
|
91
|
+
// [function]
|
|
92
|
+
// The SDK's console line: one sentence after its name, so a developer can
|
|
93
|
+
// tell it from the game's own. `warn` for a degraded state, and `error`
|
|
94
|
+
// only for a bug.
|
|
95
|
+
export function log(level, sentence, ...detail) {
|
|
96
|
+
console[level](`Arcadible SDK: ${sentence}`, ...detail);
|
|
97
|
+
}
|
|
98
|
+
// [function]
|
|
69
99
|
export function isHello(value) {
|
|
70
100
|
return (isObject(value) &&
|
|
71
101
|
value["arcadible"] === "hello" &&
|
|
@@ -79,6 +109,17 @@ export function isWelcome(value) {
|
|
|
79
109
|
value["v"] === BUS_VERSION);
|
|
80
110
|
}
|
|
81
111
|
// [function]
|
|
112
|
+
// The welcome's build as a game sees it: a copy of just its version and
|
|
113
|
+
// channel, so a page can't add fields games come to depend on, or `null`
|
|
114
|
+
// when the page doesn't say, or says it wrong.
|
|
115
|
+
export function buildOf(value) {
|
|
116
|
+
return isObject(value) &&
|
|
117
|
+
typeof value["version"] === "string" &&
|
|
118
|
+
typeof value["channel"] === "string"
|
|
119
|
+
? { version: value["version"], channel: value["channel"] }
|
|
120
|
+
: null;
|
|
121
|
+
}
|
|
122
|
+
// [function]
|
|
82
123
|
export function isMessage(value) {
|
|
83
124
|
if (!isObject(value))
|
|
84
125
|
return false;
|
package/dist/bus/server.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { type Build, 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
|
+
coalesce?: boolean;
|
|
6
7
|
};
|
|
7
8
|
export type ModuleContext = {
|
|
8
9
|
broadcast(name: string, data: unknown): void;
|
package/dist/bus/server.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// The page's end of the bus: it answers a game's requests with its modules'
|
|
2
2
|
// handlers, one at a time in order, and refuses what's too large, too many,
|
|
3
|
-
// or unknown.
|
|
3
|
+
// or unknown. It never sends more than `MAX_MESSAGE` either. `answerHellos`
|
|
4
|
+
// connects it to the play page's iframe.
|
|
4
5
|
// [import]
|
|
5
|
-
import { ArcadibleError, BUS_VERSION, Codes, isHello, isMessage, MAX_IN_FLIGHT, MAX_MESSAGE, messageSize, } from "./protocol.js";
|
|
6
|
+
import { ArcadibleError, BUS_VERSION, Codes, isHello, isMessage, log, MAX_IN_FLIGHT, MAX_MESSAGE, messageSize, } from "./protocol.js";
|
|
6
7
|
// [function]
|
|
7
8
|
function errorBody(err) {
|
|
8
9
|
if (err instanceof ArcadibleError)
|
|
9
10
|
return err.toJSON();
|
|
10
|
-
|
|
11
|
+
log("error", "a request failed.", err);
|
|
11
12
|
return {
|
|
12
13
|
code: Codes.Unavailable,
|
|
13
14
|
message: "That's unavailable right now. Try again.",
|
|
@@ -23,7 +24,7 @@ function paramsOf(value) {
|
|
|
23
24
|
export function createServer(options) {
|
|
24
25
|
const ports = new Set();
|
|
25
26
|
const inFlight = new Map();
|
|
26
|
-
//
|
|
27
|
+
// the newest request waiting its turn, by key.
|
|
27
28
|
const waiting = new Map();
|
|
28
29
|
let queue = Promise.resolve();
|
|
29
30
|
const enqueue = function (job) {
|
|
@@ -31,9 +32,20 @@ export function createServer(options) {
|
|
|
31
32
|
queue = run.catch(() => undefined);
|
|
32
33
|
return run;
|
|
33
34
|
};
|
|
35
|
+
// nothing goes to a game that's larger than a request may be: the limits
|
|
36
|
+
// keep every answer and event within it, so one that isn't is a bug.
|
|
37
|
+
const fits = function (message, what) {
|
|
38
|
+
if (messageSize(message, MAX_MESSAGE) <= MAX_MESSAGE)
|
|
39
|
+
return true;
|
|
40
|
+
log("error", `${what} was too large to send.`);
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
34
43
|
const send = function (port, name, data) {
|
|
35
|
-
if (ports.has(port))
|
|
36
|
-
|
|
44
|
+
if (!ports.has(port))
|
|
45
|
+
return;
|
|
46
|
+
const message = { kind: "event", name, data };
|
|
47
|
+
if (fits(message, name))
|
|
48
|
+
port.postMessage(message);
|
|
37
49
|
};
|
|
38
50
|
const modules = options.modules.map((make) => make({
|
|
39
51
|
broadcast(name, data) {
|
|
@@ -50,10 +62,23 @@ export function createServer(options) {
|
|
|
50
62
|
const refuse = function (port, id, error) {
|
|
51
63
|
port.postMessage({ kind: "response", id, error });
|
|
52
64
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
// answers each request in `entry`, which counted once toward the limit.
|
|
66
|
+
const respond = function ({ ids, name, port }, outcome) {
|
|
67
|
+
const count = inFlight.get(port);
|
|
68
|
+
if (count !== undefined)
|
|
69
|
+
inFlight.set(port, count - 1);
|
|
70
|
+
if (!ports.has(port))
|
|
71
|
+
return;
|
|
72
|
+
const fitting = "result" in outcome && !fits(outcome, `${name}'s answer`)
|
|
73
|
+
? {
|
|
74
|
+
error: {
|
|
75
|
+
code: Codes.Unavailable,
|
|
76
|
+
message: "That's unavailable right now. Try again.",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
: outcome;
|
|
80
|
+
for (const id of ids)
|
|
81
|
+
port.postMessage({ kind: "response", id, ...fitting });
|
|
57
82
|
};
|
|
58
83
|
const answer = function (port, request) {
|
|
59
84
|
const { id, name } = request;
|
|
@@ -65,35 +90,33 @@ export function createServer(options) {
|
|
|
65
90
|
});
|
|
66
91
|
return;
|
|
67
92
|
}
|
|
93
|
+
const params = paramsOf(request.params);
|
|
94
|
+
const key = handler.key?.(params) ?? null;
|
|
95
|
+
const pending = key === null ? undefined : waiting.get(key);
|
|
96
|
+
if (handler.coalesce &&
|
|
97
|
+
pending &&
|
|
98
|
+
pending.name === name &&
|
|
99
|
+
pending.port === port) {
|
|
100
|
+
pending.params = params;
|
|
101
|
+
pending.ids.push(id);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
68
104
|
if ((inFlight.get(port) ?? 0) >= MAX_IN_FLIGHT) {
|
|
69
105
|
refuse(port, id, {
|
|
70
106
|
code: Codes.Busy,
|
|
71
|
-
message: "Too many
|
|
107
|
+
message: "Too many calls at once. Wait for some to finish.",
|
|
72
108
|
});
|
|
73
109
|
return;
|
|
74
110
|
}
|
|
75
111
|
inFlight.set(port, (inFlight.get(port) ?? 0) + 1);
|
|
76
|
-
const
|
|
77
|
-
const key = handler.coalesce
|
|
78
|
-
? `${name}\u0000${handler.coalesce(params)}`
|
|
79
|
-
: null;
|
|
80
|
-
const pending = key === null ? undefined : waiting.get(key);
|
|
81
|
-
if (pending) {
|
|
82
|
-
pending.params = params;
|
|
83
|
-
pending.ids.push(id);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const entry = { params, ids: [id] };
|
|
112
|
+
const entry = { name, port, params, ids: [id] };
|
|
87
113
|
if (key !== null)
|
|
88
114
|
waiting.set(key, entry);
|
|
89
115
|
enqueue(() => {
|
|
90
|
-
if (key !== null)
|
|
116
|
+
if (key !== null && waiting.get(key) === entry)
|
|
91
117
|
waiting.delete(key);
|
|
92
118
|
return handler.run(entry.params);
|
|
93
|
-
}).then((result) =>
|
|
94
|
-
const error = errorBody(err);
|
|
95
|
-
entry.ids.forEach((i) => respond(port, i, { error }));
|
|
96
|
-
});
|
|
119
|
+
}).then((result) => respond(entry, { result }), (err) => respond(entry, { error: errorBody(err) }));
|
|
97
120
|
};
|
|
98
121
|
const receive = function (port, data) {
|
|
99
122
|
if (!isMessage(data) || data.kind !== "request")
|
package/dist/client.d.ts
CHANGED
|
@@ -4,10 +4,24 @@ import { type Saves } from "./saves/client.js";
|
|
|
4
4
|
export type Ready = {
|
|
5
5
|
build: Build | null;
|
|
6
6
|
};
|
|
7
|
+
/** The `arcadible` object a game calls. */
|
|
7
8
|
export interface Arcadible {
|
|
9
|
+
/** The SDK's own version. */
|
|
8
10
|
readonly version: string;
|
|
11
|
+
/**
|
|
12
|
+
* Settles once the game's saves have loaded, or after 10 seconds if they
|
|
13
|
+
* haven't, and never fails. It resolves with `build`; each module's own
|
|
14
|
+
* facts are on the module, such as `saves.where`.
|
|
15
|
+
*/
|
|
9
16
|
readonly ready: Promise<Ready>;
|
|
17
|
+
/**
|
|
18
|
+
* The build that's running, its version and channel, once `ready` has
|
|
19
|
+
* settled: compare a save's `info().version` with it, so as not to
|
|
20
|
+
* overwrite a save from a newer version. `null` when nobody says, such as
|
|
21
|
+
* standing alone, and `undefined` before.
|
|
22
|
+
*/
|
|
10
23
|
readonly build: Build | null | undefined;
|
|
24
|
+
/** Saves that carry over between the game's versions. */
|
|
11
25
|
readonly saves: Saves;
|
|
12
26
|
}
|
|
13
27
|
export type ClientOptions = {
|
package/dist/client.js
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
// same rules in its own page, when no page answers.
|
|
4
4
|
// [import]
|
|
5
5
|
import { openBus } from "./bus/client.js";
|
|
6
|
-
import { REQUEST_TIMEOUT } from "./bus/protocol.js";
|
|
6
|
+
import { buildOf, REQUEST_TIMEOUT } from "./bus/protocol.js";
|
|
7
7
|
import { indexedDBBackend } from "./saves/backend/indexeddb.js";
|
|
8
8
|
import { createSaves } from "./saves/client.js";
|
|
9
9
|
import { serve } from "./host.js";
|
|
10
10
|
import { VERSION } from "./version.js";
|
|
11
|
-
// The scope a game standing alone keeps its saves under, on its own origin
|
|
12
|
-
// Standing alone there's no version to
|
|
13
|
-
|
|
11
|
+
// The scope a game standing alone keeps its saves under, on its own origin,
|
|
12
|
+
// where only this build reads them. Standing alone there's no version to
|
|
13
|
+
// stamp: saves say `null`.
|
|
14
|
+
const STANDALONE = {
|
|
15
|
+
game: "standalone",
|
|
16
|
+
channel: "local",
|
|
17
|
+
version: null,
|
|
18
|
+
where: "build",
|
|
19
|
+
};
|
|
14
20
|
// [function]
|
|
15
21
|
export function createClient(options = {}) {
|
|
16
22
|
const backend = options.backend ?? (() => indexedDBBackend());
|
|
@@ -28,7 +34,7 @@ export function createClient(options = {}) {
|
|
|
28
34
|
let settled = null;
|
|
29
35
|
// ready once welcomed, and each module's first messages have arrived.
|
|
30
36
|
const ready = Promise.all([bus.welcomed, opened]).then(([welcome]) => {
|
|
31
|
-
settled = { build: welcome.build
|
|
37
|
+
settled = { build: buildOf(welcome.build) };
|
|
32
38
|
return settled;
|
|
33
39
|
});
|
|
34
40
|
return {
|
package/dist/host.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Server } from "./bus/server.js";
|
|
2
2
|
import type { Backend } from "./saves/backend/types.js";
|
|
3
|
+
import type { Where } from "./saves/rules.js";
|
|
3
4
|
export type ServeOptions = {
|
|
4
5
|
game: string;
|
|
5
6
|
channel: string;
|
|
6
7
|
version: string | null;
|
|
7
8
|
backend: Backend;
|
|
9
|
+
where?: Where;
|
|
8
10
|
now?: () => Date;
|
|
9
11
|
};
|
|
10
12
|
type HostOptions = ServeOptions & {
|
package/dist/host.js
CHANGED