@zaisma/game-server 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/bin/node-parts.js CHANGED
@@ -12,14 +12,29 @@ const MAXIMUM_PROFILE_COUNT = 10_000;
12
12
  /** The format the runner's saved room states are written in. A file with another is dropped, and rooms start fresh. */
13
13
  const ROOM_STATES_FORMAT_VERSION_NUMBER = 1;
14
14
 
15
- /** Fetch the public keys a JWKS URL publishes, caching them for an hour (fetched again at once when asked to refresh). */
15
+ /** A refresh asked for by a pass with a key ID not in the cache waits this long after the last fetch, so passes with made-up key IDs can't make this fetch on every hello. */
16
+ const MINIMUM_REFRESH_INTERVAL_MS = 30_000;
17
+
18
+ /**
19
+ * Fetch the public keys a JWKS URL publishes, caching them for an hour (fetched again at once when asked to refresh,
20
+ * at most once in MINIMUM_REFRESH_INTERVAL_MS). A fetch that fails keeps answering with the keys already held.
21
+ */
16
22
  function keysFrom(jwksUrl) {
17
23
  let cache = null;
24
+ let fetchedAtMs = 0;
18
25
  return async (isRefreshing = false) => {
19
- if (cache && !isRefreshing && Date.now() - cache.fetchedAtMs < KEYS_KEPT_MS) return cache.keys;
20
- const response = await fetch(jwksUrl);
21
- if (!response.ok) throw new Error(`Couldn't load Zaisma's keys from ${jwksUrl}`);
22
- cache = { fetchedAtMs: Date.now(), keys: (await response.json()).keys };
26
+ const nowMs = Date.now();
27
+ if (cache && !isRefreshing && nowMs - cache.fetchedAtMs < KEYS_KEPT_MS) return cache.keys;
28
+ if (cache && isRefreshing && nowMs - fetchedAtMs < MINIMUM_REFRESH_INTERVAL_MS) return cache.keys;
29
+ fetchedAtMs = nowMs;
30
+ try {
31
+ const response = await fetch(jwksUrl);
32
+ if (!response.ok) throw new Error(`Couldn't load Zaisma's keys from ${jwksUrl}`);
33
+ cache = { fetchedAtMs: nowMs, keys: (await response.json()).keys };
34
+ } catch (error) {
35
+ if (cache) return cache.keys;
36
+ throw error;
37
+ }
23
38
  return cache.keys;
24
39
  };
25
40
  }
package/bin/node.js CHANGED
@@ -23,8 +23,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
23
23
  import { resolve } from "node:path";
24
24
  import { realpathSync } from "node:fs";
25
25
  import { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, ERROR_TEXT, NO_ANSWER_CLOSE_CODE, PASS_ISSUER_URL, PassError, ReceiptError, REMOVED_CLOSE_CODE, REQUEST_ERROR_HTTP_STATUS, REQUEST_LIMITS, ROOM_LIMITS, ROOM_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
26
+ import { OutsideCallError } from "../src/outside-calls.js";
26
27
  import { checkedStartServerOptions, localDatabase, localRoomStates, passKeysFrom, profilesFrom, receiptKeysFrom, secretsFromEnvironment } from "./node-parts.js";
27
28
 
29
+ // Code that names OutsideCallError, as Zaisma's sandbox lets it, moves unchanged: nothing here refuses a call, so instanceof is simply false.
30
+ globalThis.OutsideCallError ??= OutsideCallError;
31
+
28
32
  const HELLO_MS = 5000;
29
33
  /**
30
34
  * How often rooms check on their players (idle, hidden, held seats, players whose time to reconnect ran
@@ -447,7 +451,7 @@ export function startServer(options) {
447
451
  const helloTimer = setTimeout(() => playerId || refuse("no_hello"), HELLO_MS);
448
452
  ws.zaismaSeenAtMs = Date.now();
449
453
 
450
- ws.on("message", async (received, isBinary) => {
454
+ const onMessage = async (received, isBinary) => {
451
455
  const text = isBinary ? null : received.toString();
452
456
  ws.zaismaSeenAtMs = Date.now();
453
457
  // Games ping every 25 seconds or so, so a connection that stops answering can be dropped.
@@ -518,6 +522,13 @@ export function startServer(options) {
518
522
  playerId = player.id;
519
523
  room.waiting = false;
520
524
  pace(code, room);
525
+ };
526
+ // Whatever a message hits, it ends that connection or that message, never the process and the matches on it.
527
+ ws.on("message", (received, isBinary) => {
528
+ onMessage(received, isBinary).catch((error) => {
529
+ console.error("Couldn't handle a message:", error);
530
+ if (!playerId) refuse("busy");
531
+ });
521
532
  });
522
533
 
523
534
  // A frame over the size limit (or a broken one) is an error on this one socket: it closes and the close handler below takes it out, the runner keeps going.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/game-server",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Run your Zaisma game server: the same rooms on Zaisma or on your own server.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://zaisma.com",
package/src/schedules.js CHANGED
@@ -159,7 +159,9 @@ const MAXIMUM_TIMER_MS = 24 * 3_600_000;
159
159
  export function startSchedules(handler, server, onFailure) {
160
160
  const timers = new Set();
161
161
  let isStopped = false;
162
- for (const schedule of handler.schedules ?? []) {
162
+ // Run from the checked list, so run.cron is the tidied cron on every runtime (get_stats lists it that way too).
163
+ const read = readSchedules(handler.schedules, typeof handler.onSchedule === "function");
164
+ for (const schedule of "schedules" in read ? read.schedules : []) {
163
165
  const plan = (afterMs) => {
164
166
  const atMs = nextRunAtMs(schedule.cron, afterMs);
165
167
  if (atMs === null || isStopped)