@arcadible/sdk 1.5.2 → 1.5.3-next.6

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 CHANGED
@@ -15,18 +15,14 @@
15
15
  <a href="https://developer.arcadible.com/build/initialization">Initialization</a> ·
16
16
  <a href="https://developer.arcadible.com/build/saves">Saves</a> ·
17
17
  <a href="https://developer.arcadible.com/build/pausing">Pausing</a> ·
18
+ <a href="https://developer.arcadible.com/build/players">Players</a> ·
18
19
  <a href="https://arcadible.com">Arcadible</a> ·
19
20
  <a href="https://studio.arcadible.com">Studio</a>
20
21
  </p>
21
22
 
22
23
  <br>
23
24
 
24
- The Arcadible SDK is optional: a game runs on [Arcadible](https://arcadible.com) without it. With it, a game gets these from the play page around it:
25
-
26
- - **[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.
27
- - **[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.
28
- - **[Pausing](https://developer.arcadible.com/build/pausing):** Arcadible tells the game when to stop, go quiet, and save, and when to carry on.
29
- - **[`system` and `context`](https://developer.arcadible.com/references/arcadible-sdk#system):** the store's language, and when this run launched.
25
+ The Arcadible SDK is optional: it's how your game works with [Arcadible](https://arcadible.com), such as [showing a loading screen](https://developer.arcadible.com/build/initialization), [keeping saves across versions](https://developer.arcadible.com/build/saves), and [knowing who's playing](https://developer.arcadible.com/build/players). Everything it does, and every call, is in the [Arcadible SDK reference](https://developer.arcadible.com/references/arcadible-sdk).
30
26
 
31
27
  ## Install
32
28
 
@@ -42,8 +38,9 @@ The package is for a game with a build step, such as a Vite project. A game with
42
38
  import { arcadible } from "@arcadible/sdk";
43
39
 
44
40
  await arcadible.initStart(); // as your code starts, before anything it awaits
41
+ // … load your files …
45
42
  arcadible.initProgress(0.5);
46
- // … load, build the scene, and draw the first frame …
43
+ // … build the scene, and draw the first frame …
47
44
  await arcadible.initComplete();
48
45
  ```
49
46
 
@@ -83,7 +80,8 @@ Arcadible pauses your game when the page is hidden, when it covers your game, an
83
80
  - [Initialization](https://developer.arcadible.com/build/initialization): where each call goes, progress, and what comes after.
84
81
  - [Saves](https://developer.arcadible.com/build/saves): using saves well, across versions, channels, and tabs, and where they're kept.
85
82
  - [Pausing](https://developer.arcadible.com/build/pausing): what a pause asks of your game, and resuming.
86
- - [Arcadible SDK](https://developer.arcadible.com/references/arcadible-sdk): including it, every call, the saves' included, `system` and `context`, and every code.
83
+ - [Players](https://developer.arcadible.com/build/players): who's playing, a guest or logged in, their name and picture.
84
+ - [Arcadible SDK](https://developer.arcadible.com/references/arcadible-sdk): including it, every call (saves and players have pages of their own), `system` and `context`, and every code.
87
85
  - [Limits](https://developer.arcadible.com/references/limits#saves): how many saves, and how large.
88
86
 
89
87
  ## License
@@ -6,6 +6,8 @@ export declare const MAX_IN_FLIGHT = 64;
6
6
  export declare const MAX_MESSAGE: number;
7
7
  export declare const Codes: {
8
8
  readonly Busy: "busy";
9
+ readonly Conflict: "conflict";
10
+ readonly Denied: "denied";
9
11
  readonly Full: "full";
10
12
  readonly Invalid: "invalid";
11
13
  readonly NotReady: "not_ready";
@@ -33,11 +33,17 @@ export const MAX_IN_FLIGHT = 64;
33
33
  export const MAX_MESSAGE = 1024 * 1024 + 64 * 1024;
34
34
  // [declare]
35
35
  // Every code a call can fail with. Worth retrying: `busy`, `timeout`, and
36
- // `unavailable`. Not: `full` (the device has no room; the player must free
37
- // some), `invalid`, `too_large`, and `too_many`. More may come: a game
36
+ // `unavailable`, and `conflict` once the value is read again. Not: `full`
37
+ // (no room, on the device or in a player's values; something must be
38
+ // freed), `denied`, `invalid`, `too_large`, and `too_many`. More may come: a game
38
39
  // treats a code it doesn't know as `unavailable`.
39
40
  export const Codes = {
40
41
  Busy: "busy",
42
+ // a value was written since the revision a write named: read it again.
43
+ Conflict: "conflict",
44
+ // Arcadible doesn't allow the call: a game reads the developer's and the
45
+ // game's values, and reads and writes its own player's, who's logged in.
46
+ Denied: "denied",
41
47
  Full: "full",
42
48
  Invalid: "invalid",
43
49
  // only `readSync`, `listSync`, and `infoSync` throw it, before `ready`.
package/dist/client.d.ts CHANGED
@@ -2,8 +2,11 @@ import { type Build } from "./bus/protocol.js";
2
2
  import { type Context } from "./context/client.js";
3
3
  import { type System } from "./system/client.js";
4
4
  import { type Players } from "./players/client.js";
5
+ import { type Functions } from "./runner/client.js";
5
6
  import type { Backend } from "./saves/backend/types.js";
6
7
  import { type Saves } from "./saves/client.js";
8
+ import { type Tickets } from "./tickets/client.js";
9
+ import { type Values } from "./values/client.js";
7
10
  export type Ready = {
8
11
  build: Build | null;
9
12
  };
@@ -47,6 +50,25 @@ export interface Arcadible {
47
50
  * `undefined` before.
48
51
  */
49
52
  readonly players: Players;
53
+ /**
54
+ * The game's function, its server code that Arcadible runs: `url`, where
55
+ * it is, fixed for the run, and `fetch(path, init)`, a request to it
56
+ * that carries the game's ticket.
57
+ */
58
+ readonly functions: Functions;
59
+ /**
60
+ * Tickets for the game's own servers: `get(server)`, proof of the game,
61
+ * the build, and the player, which a server checks.
62
+ */
63
+ readonly tickets: Tickets;
64
+ /**
65
+ * The game's data kept on Arcadible, JSON under a key: what its developer
66
+ * published (`"developer"`) and what the game shares (`"game"`), to read,
67
+ * and the player's own (`players.primary`), to read and write, wherever
68
+ * they play. `read`, `write`, `list`, and `remove`, each taking a key or
69
+ * a list of keys; `scope(scope)` for the same with the scope named once.
70
+ */
71
+ readonly values: Values;
50
72
  /**
51
73
  * Starts the game's initialization: call it as your code starts, before
52
74
  * anything it awaits, and Arcadible shows the game's loading screen until
@@ -86,5 +108,6 @@ export type ClientOptions = {
86
108
  handshakeTimeout?: number;
87
109
  requestTimeout?: number;
88
110
  backend?: () => Backend;
111
+ fetch?: (url: string, init: RequestInit) => Promise<Response>;
89
112
  };
90
113
  export declare function createClient(options?: ClientOptions): Arcadible;
package/dist/client.js CHANGED
@@ -9,8 +9,11 @@ import { createPause } from "./lifecycle/pause.js";
9
9
  import { createContextClient } from "./context/client.js";
10
10
  import { createSystemClient } from "./system/client.js";
11
11
  import { createPlayersClient } from "./players/client.js";
12
+ import { createFunctionsClient } from "./runner/client.js";
12
13
  import { indexedDBBackend } from "./saves/backend/indexeddb.js";
13
14
  import { createSaves } from "./saves/client.js";
15
+ import { createTicketsClient } from "./tickets/client.js";
16
+ import { createValuesClient } from "./values/client.js";
14
17
  import { serve } from "./host.js";
15
18
  import { VERSION } from "./version.js";
16
19
  // The scope a game standing alone keeps its saves under, on its own origin,
@@ -42,10 +45,18 @@ export function createClient(options = {}) {
42
45
  const system = createSystemClient(bus);
43
46
  const context = createContextClient(bus);
44
47
  const players = createPlayersClient(bus);
48
+ const tickets = createTicketsClient(bus);
49
+ const values = createValuesClient(bus);
50
+ const functions = createFunctionsClient(bus, {
51
+ fetch: options.fetch ?? ((url, init) => fetch(url, init)),
52
+ // set below: a request made before `ready` waits on it.
53
+ ready: () => ready,
54
+ tickets: tickets.tickets,
55
+ });
45
56
  const lifecycle = createLifecycle(bus, win);
46
57
  const pause = createPause(bus);
47
58
  // the ones `ready` waits on, or tells: lifecycle and pause do neither.
48
- const parts = [saves, system, context, players];
59
+ const parts = [saves, system, context, players, functions];
49
60
  let settled = null;
50
61
  // ready once welcomed, and each module's first messages have arrived;
51
62
  // then each is told, so its facts are known, or null.
@@ -72,6 +83,9 @@ export function createClient(options = {}) {
72
83
  return context.value;
73
84
  },
74
85
  players: players.players,
86
+ functions: functions.functions,
87
+ tickets: tickets.tickets,
88
+ values: values.values,
75
89
  initStart() {
76
90
  lifecycle.start();
77
91
  return ready;
package/dist/host.d.ts CHANGED
@@ -7,8 +7,11 @@ import { type System } from "./system/server.js";
7
7
  import { type LifecycleHost } from "./lifecycle/server.js";
8
8
  import { type SignedIn } from "./players/host.js";
9
9
  import { type PlayersHost } from "./players/server.js";
10
+ import type { RunnerState } from "./runner/rules.js";
10
11
  import type { Backend } from "./saves/backend/types.js";
11
12
  import type { Where } from "./saves/rules.js";
13
+ import { type TicketsHost } from "./tickets/server.js";
14
+ import { type ValuesHost } from "./values/server.js";
12
15
  export type ServeOptions = {
13
16
  game: string;
14
17
  channel: string;
@@ -21,6 +24,9 @@ export type ServeOptions = {
21
24
  system?: System;
22
25
  context?: ContextState;
23
26
  players?: PlayersHost;
27
+ runner?: RunnerState;
28
+ tickets?: TicketsHost;
29
+ values?: ValuesHost;
24
30
  };
25
31
  export type Host = {
26
32
  close(): void;
@@ -31,6 +37,7 @@ export type Host = {
31
37
  type HostOptions = Omit<ServeOptions, "holds" | "system" | "context" | "players"> & {
32
38
  player?: SignedIn | null;
33
39
  fetch?: (url: string) => Promise<Response>;
40
+ logIn?: (() => Promise<void>) | undefined;
34
41
  language?: string;
35
42
  launchedAt?: string;
36
43
  frame: HTMLIFrameElement;
package/dist/host.js CHANGED
@@ -11,7 +11,10 @@ import { createSystem } from "./system/server.js";
11
11
  import { lifecycleServer } from "./lifecycle/server.js";
12
12
  import { playersHost } from "./players/host.js";
13
13
  import { playersServer } from "./players/server.js";
14
+ import { runnerServer } from "./runner/server.js";
14
15
  import { savesServer } from "./saves/server.js";
16
+ import { ticketsServer } from "./tickets/server.js";
17
+ import { valuesServer } from "./values/server.js";
15
18
  // [function]
16
19
  // Answers the bus for one game in one channel, on any number of ports.
17
20
  export function serve(options) {
@@ -25,6 +28,7 @@ export function serve(options) {
25
28
  ...(options.system ? [options.system.module] : []),
26
29
  ...(options.context ? [contextServer(options.context)] : []),
27
30
  ...(options.players ? [playersServer(options.players)] : []),
31
+ ...(options.runner ? [runnerServer(options.runner)] : []),
28
32
  savesServer({
29
33
  backend,
30
34
  now: options.now ?? (() => new Date()),
@@ -32,6 +36,8 @@ export function serve(options) {
32
36
  version,
33
37
  where: options.where ?? "game",
34
38
  }),
39
+ ...(options.tickets ? [ticketsServer(options.tickets)] : []),
40
+ ...(options.values ? [valuesServer(options.values)] : []),
35
41
  ...(options.lifecycle ? [lifecycleServer(options.lifecycle)] : []),
36
42
  ],
37
43
  });
@@ -74,6 +80,7 @@ export function createHost(options) {
74
80
  backend: options.backend,
75
81
  fetch: options.fetch ?? ((url) => win.fetch(url)),
76
82
  game: options.game,
83
+ logIn: options.logIn,
77
84
  player: options.player,
78
85
  }),
79
86
  }),
package/dist/index.d.ts CHANGED
@@ -7,5 +7,8 @@ export type { Saves } from "./saves/client.js";
7
7
  export type { System } from "./system/client.js";
8
8
  export type { Context } from "./context/client.js";
9
9
  export type { Players } from "./players/client.js";
10
- export type { Player } from "./players/rules.js";
10
+ export type { Player, PlayerId } from "./players/rules.js";
11
+ export type { Functions } from "./runner/client.js";
12
+ export type { Tickets } from "./tickets/client.js";
13
+ export type { Entry, Item, Listed, ListOptions, ReadableValues, Scope, ScopedValues, Value, Values, Written, } from "./values/client.js";
11
14
  export type { SaveInfo } from "./saves/rules.js";
@@ -1,5 +1,5 @@
1
1
  import type { Bus } from "../bus/client.js";
2
- import { type Player } from "./rules.js";
2
+ import { type Player, type PlayerId } from "./rules.js";
3
3
  /** Who's playing. */
4
4
  export interface Players {
5
5
  /**
@@ -11,11 +11,21 @@ export interface Players {
11
11
  readonly primary: Player | null | undefined;
12
12
  /**
13
13
  * Each player's picture, 256 × 256, as WebP, in the order given: a Blob,
14
- * or `null` for a player with none (a guest always). Takes players or
15
- * their ids, up to 50. Rejects with `unavailable` when the pictures can't
16
- * be reached, as offline.
14
+ * or `null` for a player with none (a guest always), and for a missing
15
+ * player, such as `primary` off Arcadible. Takes players or their ids, up
16
+ * to 50. Rejects with `unavailable` when the pictures can't be reached, as
17
+ * offline.
17
18
  */
18
- pictures(players: readonly (Player | string)[]): Promise<(Blob | null)[]>;
19
+ pictures(players: readonly (Player | PlayerId | null | undefined)[]): Promise<(Blob | null)[]>;
20
+ /**
21
+ * Has Arcadible offer a guest a log-in, in a small notice of its own over
22
+ * the game: call it when a player asks to log in, or reaches something
23
+ * that needs an account. Resolves once the offer is up, or at once for a
24
+ * player who's logged in already; it never says whether they took it,
25
+ * since logging in ends the run, and the next one starts as them. Rejects
26
+ * with `unavailable` where no log-in can be offered, as off Arcadible.
27
+ */
28
+ logIn(): Promise<void>;
19
29
  }
20
30
  export type PlayersClient = {
21
31
  players: Players;
@@ -1,7 +1,7 @@
1
1
  // The game's side of `players`: `arcadible.players`, who's playing. Its
2
2
  // `primary` is a fact, fixed for the run: `undefined` before `ready`, and
3
3
  // `null` where no page says (standing alone, or an older page). Pictures
4
- // are asked for when a game draws them.
4
+ // are asked for when a game draws them, and a log-in is the page's to offer.
5
5
  import { ArcadibleError, Codes, isObject, log } from "../bus/protocol.js";
6
6
  import { fact } from "../facts.js";
7
7
  import { isId, isPicture, MAX_PICTURES, Names, PICTURES_PER_REQUEST, stateOf, } from "./rules.js";
@@ -11,7 +11,8 @@ function isAnswer(length) {
11
11
  return (value) => Array.isArray(value) && value.length === length;
12
12
  }
13
13
  // [function]
14
- // the ids a game asked for, checked before anything is sent.
14
+ // the ids a game asked for, checked before anything is sent: `null` for a
15
+ // missing player, such as `primary` off Arcadible, which has no picture.
15
16
  function idsOf(players) {
16
17
  if (!Array.isArray(players)) {
17
18
  throw new ArcadibleError(Codes.Invalid, "players.pictures takes an array of players, or of their ids.");
@@ -20,6 +21,8 @@ function idsOf(players) {
20
21
  throw new ArcadibleError(Codes.TooMany, `players.pictures takes up to ${MAX_PICTURES} players at once, not ${players.length}.`);
21
22
  }
22
23
  return players.map((entry, index) => {
24
+ if (entry === null || entry === undefined)
25
+ return null;
23
26
  const id = isObject(entry) ? entry["id"] : entry;
24
27
  if (!isId(id)) {
25
28
  throw new ArcadibleError(Codes.Invalid, `players.pictures takes players, or their ids: entry ${index} is neither.`);
@@ -64,12 +67,33 @@ export function createPlayersClient(bus) {
64
67
  },
65
68
  async pictures(list) {
66
69
  const ids = idsOf(list);
70
+ // only the players there are: a missing one is no picture, and isn't
71
+ // asked for.
72
+ const wanted = ids.filter((id) => id !== null);
67
73
  // a few at a time, so every answer fits a message.
68
- const pictures = [];
69
- for (let at = 0; at < ids.length; at += PICTURES_PER_REQUEST) {
70
- pictures.push(...(await ask(ids.slice(at, at + PICTURES_PER_REQUEST))));
74
+ const found = [];
75
+ for (let at = 0; at < wanted.length; at += PICTURES_PER_REQUEST) {
76
+ found.push(...(await ask(wanted.slice(at, at + PICTURES_PER_REQUEST))));
77
+ }
78
+ let next = 0;
79
+ return ids.map((id) => (id === null ? null : (found[next++] ?? null)));
80
+ },
81
+ async logIn() {
82
+ // logged in already: there's nothing to offer.
83
+ if (primary.value && !primary.value.guest)
84
+ return;
85
+ try {
86
+ await bus.request(Names.LogIn, {}, isObject);
87
+ }
88
+ catch (err) {
89
+ // no page to ask (standing alone), one from before log-ins, or one
90
+ // that didn't answer in time: no log-in is offered now.
91
+ if (err instanceof ArcadibleError &&
92
+ (err.code === Codes.Unsupported || err.code === Codes.Timeout)) {
93
+ throw new ArcadibleError(Codes.Unavailable, "A log-in can't be offered here.");
94
+ }
95
+ throw err;
71
96
  }
72
- return pictures;
73
97
  },
74
98
  };
75
99
  return { players, settle: () => primary.settle() };
@@ -10,5 +10,6 @@ export type PlayersHostOptions = {
10
10
  backend: Backend;
11
11
  game: string;
12
12
  fetch: (url: string) => Promise<Response>;
13
+ logIn?: (() => Promise<void>) | undefined;
13
14
  };
14
15
  export declare function playersHost(options: PlayersHostOptions): PlayersHost;
@@ -1,7 +1,7 @@
1
1
  // Who a play page's game runs for: the player signed in, from the page's
2
2
  // own lookup, or the device's guest in this game.
3
3
  // [import]
4
- import { log } from "../bus/protocol.js";
4
+ import { ArcadibleError, Codes, log } from "../bus/protocol.js";
5
5
  import { isId } from "./rules.js";
6
6
  // [function]
7
7
  // Only the primary's picture is found, fetched from its address when a
@@ -46,5 +46,14 @@ export function playersHost(options) {
46
46
  });
47
47
  return picture;
48
48
  },
49
+ async logIn() {
50
+ // signed in already: nothing to offer.
51
+ if (signedIn)
52
+ return;
53
+ if (!options.logIn) {
54
+ throw new ArcadibleError(Codes.Unavailable, "A log-in can't be offered here.");
55
+ }
56
+ await options.logIn();
57
+ },
49
58
  };
50
59
  }
@@ -1,12 +1,15 @@
1
1
  export declare const Names: {
2
2
  readonly State: "players.state";
3
3
  readonly Pictures: "players.pictures";
4
+ readonly LogIn: "players.logIn";
4
5
  };
6
+ /** A player's id in this game: a UUID, in lowercase. */
7
+ export type PlayerId = string;
5
8
  /** A player, as a game sees one. */
6
9
  export interface Player {
7
10
  /** This player in this game: a UUID, the same every run, different in every game. */
8
- readonly id: string;
9
- /** Signed out: playing on this device, with no account. */
11
+ readonly id: PlayerId;
12
+ /** Logged out: playing on this device, with no account. */
10
13
  readonly guest: boolean;
11
14
  /** The profile's name, as players see it; null for a guest, or with none. */
12
15
  readonly name: string | null;
@@ -19,7 +22,7 @@ export declare const PICTURES_PER_REQUEST = 10;
19
22
  export declare const MAX_PICTURE: number;
20
23
  export declare const PICTURE_TYPE = "image/webp";
21
24
  /** Whether a value is a player's id: a UUID, in lowercase. */
22
- export declare function isId(value: unknown): value is string;
25
+ export declare function isId(value: unknown): value is PlayerId;
23
26
  /**
24
27
  * A new id, for a guest: a random UUID. `crypto.randomUUID` isn't on
25
28
  * plain-http pages, as a development play page is, so it's built from
@@ -1,5 +1,5 @@
1
1
  // `players` on the bus, shared by both ends: who's playing, fixed for the
2
- // run, and their pictures, on demand.
2
+ // run, their pictures, on demand, and a log-in, offered to a guest.
3
3
  // [import]
4
4
  import { isObject } from "../bus/protocol.js";
5
5
  // [declare]
@@ -8,6 +8,9 @@ export const Names = {
8
8
  State: "players.state",
9
9
  // pictures, a request with `{ ids }`, answered with a Blob or null each.
10
10
  Pictures: "players.pictures",
11
+ // a log-in, offered to a guest by the page: a request with nothing,
12
+ // answered once the offer is up.
13
+ LogIn: "players.logIn",
11
14
  };
12
15
  // The most pictures one call asks for.
13
16
  export const MAX_PICTURES = 50;
@@ -3,5 +3,6 @@ import { type Player } from "./rules.js";
3
3
  export type PlayersHost = {
4
4
  primary(): Promise<Player>;
5
5
  picture(id: string): Promise<Blob | null>;
6
+ logIn?(): Promise<void>;
6
7
  };
7
8
  export declare function playersServer(host: PlayersHost): () => Module;
@@ -1,5 +1,6 @@
1
1
  // The page's side of `players`: who's playing, sent once as the game
2
- // connects, and their pictures, fetched when a game asks. It never changes
2
+ // connects, their pictures, fetched when a game asks, and a log-in, which
3
+ // the page offers a guest when a game asks. Who's playing never changes
3
4
  // during the run: the page ends the run when the player does.
4
5
  // [import]
5
6
  import { ArcadibleError, Codes, log } from "../bus/protocol.js";
@@ -36,8 +37,25 @@ export function playersServer(host) {
36
37
  return null;
37
38
  });
38
39
  };
40
+ const logIn = async function () {
41
+ try {
42
+ if (!host.logIn)
43
+ throw new Error("The page offers no log-in.");
44
+ await host.logIn();
45
+ }
46
+ catch (err) {
47
+ // whatever the page's reason, the game is told one thing.
48
+ if (!(err instanceof ArcadibleError)) {
49
+ log("warn", "a log-in couldn't be offered.", err);
50
+ }
51
+ throw new ArcadibleError(Codes.Unavailable, "A log-in can't be offered here.");
52
+ }
53
+ return {};
54
+ };
39
55
  return () => ({
40
56
  handlers: {
57
+ // beside the queue: it waits on the page, not on storage.
58
+ [Names.LogIn]: { run: logIn, parallel: true },
41
59
  // beside the queue: it waits on the network, not on storage.
42
60
  [Names.Pictures]: { run: pictures, parallel: true },
43
61
  },
@@ -0,0 +1,34 @@
1
+ import type { Bus } from "../bus/client.js";
2
+ import type { Tickets } from "../tickets/client.js";
3
+ /** The game's function: its server code, which Arcadible runs. */
4
+ export interface Functions {
5
+ /**
6
+ * Its address, for a request of the game's own making: `${url}/score`,
7
+ * with a ticket for it (`tickets.get(url)`). Set by the time `ready`
8
+ * settles; `null` for a game with no function, or where no page says,
9
+ * such as standing alone, and `undefined` before.
10
+ */
11
+ readonly url: string | null | undefined;
12
+ /**
13
+ * A request to the game's function, as `fetch` makes one: `path` from
14
+ * its root, such as `"/score"`, and `init` as `fetch` takes it. It
15
+ * carries a ticket, which says the game, the build, and the player, so
16
+ * the function knows who's asking; the page keeps the ticket while it's
17
+ * good, and gets another when it isn't. Resolves with the function's
18
+ * `Response`, whatever its status. Rejects with `invalid` (a path that
19
+ * isn't one), `unavailable` (no function here, or no ticket right now),
20
+ * or `busy` (too many tickets), and as `fetch` does when the network
21
+ * fails.
22
+ */
23
+ fetch(path: string, init?: RequestInit): Promise<Response>;
24
+ }
25
+ export type FunctionsClient = {
26
+ functions: Functions;
27
+ settle(): void;
28
+ };
29
+ export type FunctionsOptions = {
30
+ fetch: (url: string, init: RequestInit) => Promise<Response>;
31
+ ready: () => Promise<unknown>;
32
+ tickets: Tickets;
33
+ };
34
+ export declare function createFunctionsClient(bus: Bus, options: FunctionsOptions): FunctionsClient;
@@ -0,0 +1,45 @@
1
+ // The game's side of `functions`: `arcadible.functions`, the game's
2
+ // function (its runner, inside Arcadible), the server code Arcadible runs
3
+ // for it. `url` is where it is, fixed for the run: `undefined` before
4
+ // `ready`, and `null` where no page says (standing alone, an older page, or
5
+ // a game with no function). `fetch` calls it, with the ticket that proves
6
+ // who's asking.
7
+ import { ArcadibleError, Codes } from "../bus/protocol.js";
8
+ import { fact } from "../facts.js";
9
+ import { Names, runnerOf } from "./rules.js";
10
+ // [function]
11
+ export function createFunctionsClient(bus, options) {
12
+ const where = fact(bus, Names.State, {
13
+ fixed: true,
14
+ parse(data) {
15
+ const runner = runnerOf(data);
16
+ return runner ? Object.freeze({ url: runner.url }) : null;
17
+ },
18
+ });
19
+ const functions = {
20
+ get url() {
21
+ const value = where.value;
22
+ return value ? value.url : value;
23
+ },
24
+ async fetch(path, init = {}) {
25
+ if (typeof path !== "string" || !path.startsWith("/")) {
26
+ throw new ArcadibleError(Codes.Invalid, 'functions.fetch takes a path from the function\'s root, such as "/score".');
27
+ }
28
+ await options.ready();
29
+ const url = where.value?.url;
30
+ if (!url) {
31
+ throw new ArcadibleError(Codes.Unavailable, "This game has no function here.");
32
+ }
33
+ const ticket = await options.tickets.get(url);
34
+ const headers = new Headers(init.headers);
35
+ headers.set("Authorization", `Bearer ${ticket}`);
36
+ return options.fetch(`${url}${path}`, { ...init, headers });
37
+ },
38
+ };
39
+ return {
40
+ functions,
41
+ settle() {
42
+ where.settle();
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,12 @@
1
+ export declare const Names: {
2
+ readonly State: "runner.state";
3
+ };
4
+ export type RunnerState = {
5
+ url: string;
6
+ };
7
+ /**
8
+ * The runner as the game keeps it: its address, a web address with no
9
+ * trailing slash, so `${url}/score` reads as it's written. Null for
10
+ * anything else.
11
+ */
12
+ export declare function runnerOf(value: unknown): RunnerState | null;
@@ -0,0 +1,36 @@
1
+ // `runner` on the bus, shared by both ends: where the game's runner is (its
2
+ // function, as a game knows it), fixed for the run.
3
+ // [import]
4
+ import { isObject } from "../bus/protocol.js";
5
+ // [declare]
6
+ export const Names = {
7
+ // the runner, `{ url }`: an event, sent once, as the module opens.
8
+ State: "runner.state",
9
+ };
10
+ // The longest address kept.
11
+ const MAX_URL = 2048;
12
+ // [function]
13
+ /**
14
+ * The runner as the game keeps it: its address, a web address with no
15
+ * trailing slash, so `${url}/score` reads as it's written. Null for
16
+ * anything else.
17
+ */
18
+ export function runnerOf(value) {
19
+ if (!isObject(value))
20
+ return null;
21
+ const url = value["url"];
22
+ if (typeof url !== "string" || url.length > MAX_URL)
23
+ return null;
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(url);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
32
+ return null;
33
+ if (parsed.search !== "" || parsed.hash !== "")
34
+ return null;
35
+ return { url: `${parsed.origin}${parsed.pathname.replace(/\/+$/u, "")}` };
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { Module } from "../bus/server.js";
2
+ import { type RunnerState } from "./rules.js";
3
+ export declare function runnerServer(runner: RunnerState): () => Module;
@@ -0,0 +1,20 @@
1
+ // The page's side of `runner`: where the game's runner is, sent once as the
2
+ // game connects. It never changes during the run.
3
+ // [import]
4
+ import { log } from "../bus/protocol.js";
5
+ import { Names, runnerOf } from "./rules.js";
6
+ // [function]
7
+ export function runnerServer(runner) {
8
+ const state = runnerOf(runner);
9
+ if (!state) {
10
+ log("error", `the runner's address isn't one: ${JSON.stringify(runner)}.`);
11
+ }
12
+ return () => ({
13
+ handlers: {},
14
+ // first on each channel, so the game has it by `ready`.
15
+ async open(send) {
16
+ if (state)
17
+ send(Names.State, state);
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ import type { Bus } from "../bus/client.js";
2
+ /** Tickets for the game's own servers. */
3
+ export interface Tickets {
4
+ /**
5
+ * A ticket for one of the game's servers: proof, signed by Arcadible, of
6
+ * the game, the build, and the player (no one, for a guest), good for a
7
+ * few minutes. Takes the server's address, any URL of it, which
8
+ * the game declared in `arcadible.toml` (`[client] server_origins`). Send it
9
+ * once, as the game connects: an `Authorization` header, or a WebSocket's
10
+ * first message, never in a URL. Call it each time one is needed; the
11
+ * page keeps it while it's good. Rejects with `invalid` (not an address,
12
+ * or not one of the game's servers), `busy` (too many), or `unavailable`
13
+ * (offline, or off Arcadible; worth trying again).
14
+ */
15
+ get(server: string): Promise<string>;
16
+ }
17
+ export type TicketsClient = {
18
+ tickets: Tickets;
19
+ };
20
+ export declare function createTicketsClient(bus: Bus): TicketsClient;