@arcadible/sdk 1.5.0-next.0 → 1.5.0-next.2

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/dist/client.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type Build } from "./bus/protocol.js";
2
+ import { type Context } from "./context/client.js";
3
+ import { type System } from "./system/client.js";
2
4
  import type { Backend } from "./saves/backend/types.js";
3
5
  import { type Saves } from "./saves/client.js";
4
6
  export type Ready = {
@@ -23,6 +25,20 @@ export interface Arcadible {
23
25
  readonly build: Build | null | undefined;
24
26
  /** Saves that carry over between the game's versions. */
25
27
  readonly saves: Saves;
28
+ /**
29
+ * Arcadible's state for the player, which changes while the game runs and
30
+ * asks nothing of it: `language`, with `on("change")`. Set by the time
31
+ * `ready` settles; `null` where no page says, such as standing alone, and
32
+ * `undefined` before.
33
+ */
34
+ readonly system: System | null | undefined;
35
+ /**
36
+ * How this run started, fixed for the run: `launch.at`, Arcadible's time
37
+ * at launch, so a daily puzzle doesn't trust the device's clock. Set by
38
+ * the time `ready` settles; `null` where no page says, such as standing
39
+ * alone, and `undefined` before.
40
+ */
41
+ readonly context: Context | null | undefined;
26
42
  /**
27
43
  * Starts the game's initialization: call it as your code starts, before
28
44
  * anything it awaits, and Arcadible shows the game's loading screen until
package/dist/client.js CHANGED
@@ -6,6 +6,8 @@ import { openBus } from "./bus/client.js";
6
6
  import { buildOf, REQUEST_TIMEOUT } from "./bus/protocol.js";
7
7
  import { createLifecycle } from "./lifecycle/client.js";
8
8
  import { createPause } from "./lifecycle/pause.js";
9
+ import { createContextClient } from "./context/client.js";
10
+ import { createSystemClient } from "./system/client.js";
9
11
  import { indexedDBBackend } from "./saves/backend/indexeddb.js";
10
12
  import { createSaves } from "./saves/client.js";
11
13
  import { serve } from "./host.js";
@@ -35,8 +37,12 @@ export function createClient(options = {}) {
35
37
  timeout: options.requestTimeout ?? REQUEST_TIMEOUT,
36
38
  });
37
39
  let settled = null;
40
+ const system = createSystemClient(bus);
41
+ const context = createContextClient(bus);
38
42
  // ready once welcomed, and each module's first messages have arrived.
39
43
  const ready = Promise.all([bus.welcomed, opened]).then(([welcome]) => {
44
+ system.settle();
45
+ context.settle();
40
46
  settled = { build: buildOf(welcome.build) };
41
47
  return settled;
42
48
  });
@@ -49,6 +55,12 @@ export function createClient(options = {}) {
49
55
  return settled ? settled.build : undefined;
50
56
  },
51
57
  saves,
58
+ get system() {
59
+ return system.value;
60
+ },
61
+ get context() {
62
+ return context.value;
63
+ },
52
64
  initStart() {
53
65
  lifecycle.start();
54
66
  return ready;
@@ -0,0 +1,12 @@
1
+ import type { Bus } from "../bus/client.js";
2
+ /** How this run started, fixed for the run. */
3
+ export interface Context {
4
+ readonly launch: {
5
+ /** This launch, by Arcadible's clock, as an ISO string. */
6
+ readonly at: string;
7
+ };
8
+ }
9
+ export declare function createContextClient(bus: Bus): {
10
+ settle(): void;
11
+ readonly value: Context | null | undefined;
12
+ };
@@ -0,0 +1,31 @@
1
+ // The game's side of `context`: `arcadible.context`, how this run started,
2
+ // fixed for the run. `undefined` before `ready`, and `null` where no page
3
+ // says (standing alone, or an older page).
4
+ import { isObject, log } from "../bus/protocol.js";
5
+ import { contextOf, Names } from "./rules.js";
6
+ // [function]
7
+ export function createContextClient(bus) {
8
+ let context = null;
9
+ let settled = false;
10
+ bus.on(Names.State, isObject, (data) => {
11
+ // fixed for the run: the first one stands.
12
+ if (context)
13
+ return;
14
+ const next = contextOf(data);
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;
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,12 @@
1
+ export declare const Names: {
2
+ readonly State: "context.state";
3
+ };
4
+ export type ContextState = {
5
+ launch: {
6
+ at: string;
7
+ };
8
+ };
9
+ /** A time the page may send: an ISO string in UTC that reads as a date. */
10
+ export declare function isTime(value: unknown): value is string;
11
+ /** The context as the game keeps it: only fields it knows, each checked. */
12
+ export declare function contextOf(value: unknown): ContextState | null;
@@ -0,0 +1,29 @@
1
+ // `context` on the bus, shared by both ends: how this run started, fixed
2
+ // for the run.
3
+ // [import]
4
+ import { isObject } from "../bus/protocol.js";
5
+ // [declare]
6
+ export const Names = {
7
+ // the context, `{ launch: { at } }`: an event, sent once, as the module
8
+ // opens.
9
+ State: "context.state",
10
+ };
11
+ // a time as JavaScript's toISOString() writes it, in UTC.
12
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/u;
13
+ // [function]
14
+ /** A time the page may send: an ISO string in UTC that reads as a date. */
15
+ export function isTime(value) {
16
+ return (typeof value === "string" &&
17
+ ISO.test(value) &&
18
+ !Number.isNaN(Date.parse(value)));
19
+ }
20
+ // [function]
21
+ /** The context as the game keeps it: only fields it knows, each checked. */
22
+ export function contextOf(value) {
23
+ if (!isObject(value))
24
+ return null;
25
+ const launch = value["launch"];
26
+ if (!isObject(launch) || !isTime(launch["at"]))
27
+ return null;
28
+ return { launch: { at: launch["at"] } };
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { Module } from "../bus/server.js";
2
+ import { type ContextState } from "./rules.js";
3
+ export declare function contextServer(context: ContextState): () => Module;
@@ -0,0 +1,13 @@
1
+ // The page's side of `context`: how this run started, sent once as the game
2
+ // connects. It never changes during the run.
3
+ import { Names } from "./rules.js";
4
+ // [function]
5
+ export function contextServer(context) {
6
+ return () => ({
7
+ handlers: {},
8
+ // first on each channel, so the game has it by `ready`.
9
+ async open(send) {
10
+ send(Names.State, context);
11
+ },
12
+ });
13
+ }
package/dist/host.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { Hello } from "./bus/protocol.js";
2
2
  import { type Server } from "./bus/server.js";
3
+ import type { ContextState } from "./context/rules.js";
3
4
  import { type Holds } from "./lifecycle/holds.js";
5
+ import { type SystemState } from "./system/rules.js";
6
+ import { type System } from "./system/server.js";
4
7
  import { type LifecycleHost } from "./lifecycle/server.js";
5
8
  import type { Backend } from "./saves/backend/types.js";
6
9
  import type { Where } from "./saves/rules.js";
@@ -13,13 +16,18 @@ export type ServeOptions = {
13
16
  now?: () => Date;
14
17
  lifecycle?: LifecycleHost;
15
18
  holds?: Holds;
19
+ system?: System;
20
+ context?: ContextState;
16
21
  };
17
22
  export type Host = {
18
23
  close(): void;
19
24
  hold(): () => void;
20
25
  leave(timeout?: number): Promise<void>;
26
+ setSystem(state: Partial<SystemState>): void;
21
27
  };
22
- type HostOptions = Omit<ServeOptions, "holds"> & {
28
+ type HostOptions = Omit<ServeOptions, "holds" | "system" | "context"> & {
29
+ language?: string;
30
+ launchedAt?: string;
23
31
  frame: HTMLIFrameElement;
24
32
  origin: string;
25
33
  window?: Window;
@@ -29,5 +37,6 @@ export declare function serve(options: ServeOptions): Server;
29
37
  export declare function createHost(options: HostOptions): Host;
30
38
  export type { Server } from "./bus/server.js";
31
39
  export { createHolds, type Holds } from "./lifecycle/holds.js";
40
+ export { createSystem, type System } from "./system/server.js";
32
41
  export { indexedDBBackend } from "./saves/backend/indexeddb.js";
33
42
  export { memoryBackend } from "./saves/backend/memory.js";
package/dist/host.js CHANGED
@@ -1,7 +1,10 @@
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
3
  import { answerHellos, createServer } from "./bus/server.js";
4
+ import { contextServer } from "./context/server.js";
4
5
  import { createHolds } from "./lifecycle/holds.js";
6
+ import { shortLanguage } from "./system/rules.js";
7
+ import { createSystem } from "./system/server.js";
5
8
  import { lifecycleServer } from "./lifecycle/server.js";
6
9
  import { savesServer } from "./saves/server.js";
7
10
  // [function]
@@ -11,8 +14,11 @@ export function serve(options) {
11
14
  return createServer({
12
15
  build: version === null ? null : { version, channel },
13
16
  modules: [
14
- // first, so the game knows whether it's held by `ready`.
17
+ // first, so the game knows whether it's held, and the system's
18
+ // state, by `ready`.
15
19
  ...(options.holds ? [options.holds.module] : []),
20
+ ...(options.system ? [options.system.module] : []),
21
+ ...(options.context ? [contextServer(options.context)] : []),
16
22
  savesServer({
17
23
  backend,
18
24
  now: options.now ?? (() => new Date()),
@@ -33,10 +39,20 @@ export function serve(options) {
33
39
  export function createHost(options) {
34
40
  const win = options.window ?? window;
35
41
  const holds = createHolds();
42
+ const system = createSystem({
43
+ language: options.language ?? shortLanguage(win.navigator?.language),
44
+ });
36
45
  const answering = answerHellos({
37
46
  frame: options.frame,
38
47
  origin: options.origin,
39
- server: serve({ ...options, holds }),
48
+ server: serve({
49
+ ...options,
50
+ holds,
51
+ system,
52
+ context: {
53
+ launch: { at: options.launchedAt ?? new Date().toISOString() },
54
+ },
55
+ }),
40
56
  window: win,
41
57
  onHello: options.onHello,
42
58
  });
@@ -60,8 +76,10 @@ export function createHost(options) {
60
76
  },
61
77
  hold: () => holds.hold(),
62
78
  leave: (timeout) => holds.leave(timeout),
79
+ setSystem: (state) => system.set(state),
63
80
  };
64
81
  }
65
82
  export { createHolds } from "./lifecycle/holds.js";
83
+ export { createSystem } from "./system/server.js";
66
84
  export { indexedDBBackend } from "./saves/backend/indexeddb.js";
67
85
  export { memoryBackend } from "./saves/backend/memory.js";
package/dist/index.d.ts CHANGED
@@ -4,4 +4,6 @@ export { ArcadibleError, Codes } from "./bus/protocol.js";
4
4
  export type { Build, Code } from "./bus/protocol.js";
5
5
  export type { Arcadible, Ready } from "./client.js";
6
6
  export type { Saves } from "./saves/client.js";
7
+ export type { System } from "./system/client.js";
8
+ export type { Context } from "./context/client.js";
7
9
  export type { SaveInfo } from "./saves/rules.js";
@@ -0,0 +1,15 @@
1
+ import type { Bus } from "../bus/client.js";
2
+ /** Arcadible's state for the player, which changes while the game runs. */
3
+ export interface System {
4
+ /** The store's language, a short tag: "en", "ja". */
5
+ readonly language: string;
6
+ /**
7
+ * Called when something changes, with the new state (this same object)
8
+ * and the names of what changed. Returns a function that unsubscribes.
9
+ */
10
+ on(name: "change", listener: (system: System, changed: readonly string[]) => void): () => void;
11
+ }
12
+ export declare function createSystemClient(bus: Bus): {
13
+ settle(): void;
14
+ readonly value: System | null | undefined;
15
+ };
@@ -0,0 +1,62 @@
1
+ // The game's side of `system`: `arcadible.system`, Arcadible's state for the
2
+ // player, read at any time, with one `change` event naming what changed.
3
+ // `undefined` before `ready`, and `null` where no page says (standing
4
+ // alone, or an older page).
5
+ import { ArcadibleError, Codes, isObject, log } from "../bus/protocol.js";
6
+ import { Names, stateOf } from "./rules.js";
7
+ // [function]
8
+ export function createSystemClient(bus) {
9
+ let state = null;
10
+ let settled = false;
11
+ const listeners = new Set();
12
+ const system = {
13
+ get language() {
14
+ return state.language;
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
+ };
28
+ },
29
+ };
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
+ return {
53
+ settle() {
54
+ settled = true;
55
+ },
56
+ get value() {
57
+ if (!settled)
58
+ return undefined;
59
+ return state ? system : null;
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,12 @@
1
+ export declare const Names: {
2
+ readonly State: "system.state";
3
+ };
4
+ export type SystemState = {
5
+ language: string;
6
+ };
7
+ /** A language the page may send: a short, lowercase tag. */
8
+ export declare function isLanguage(value: unknown): value is string;
9
+ /** The state as the game keeps it: only fields it knows, each checked. */
10
+ export declare function stateOf(value: unknown): SystemState | null;
11
+ /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
12
+ export declare function shortLanguage(tag: string | undefined): string;
@@ -0,0 +1,30 @@
1
+ // `system` on the bus, shared by both ends: Arcadible's state for the
2
+ // player, which changes while the game runs and asks nothing of it.
3
+ // [import]
4
+ import { isObject } from "../bus/protocol.js";
5
+ // [declare]
6
+ export const Names = {
7
+ // the state, `{ language }`: an event, sent as the module opens and on
8
+ // every change.
9
+ State: "system.state",
10
+ };
11
+ // a language as the store names it: a short tag, `en` or `ja`.
12
+ const LANGUAGE = /^[a-z]{2,3}$/u;
13
+ // [function]
14
+ /** A language the page may send: a short, lowercase tag. */
15
+ export function isLanguage(value) {
16
+ return typeof value === "string" && LANGUAGE.test(value);
17
+ }
18
+ // [function]
19
+ /** The state as the game keeps it: only fields it knows, each checked. */
20
+ export function stateOf(value) {
21
+ if (!isObject(value) || !isLanguage(value["language"]))
22
+ return null;
23
+ return { language: value["language"] };
24
+ }
25
+ // [function]
26
+ /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
27
+ export function shortLanguage(tag) {
28
+ const short = (tag ?? "").split("-")[0]?.toLowerCase() ?? "";
29
+ return isLanguage(short) ? short : "en";
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { Module, ModuleContext } from "../bus/server.js";
2
+ import { type SystemState } from "./rules.js";
3
+ export type System = {
4
+ set(state: Partial<SystemState>): void;
5
+ module(context: ModuleContext): Module;
6
+ };
7
+ export declare function createSystem(initial: SystemState): System;
@@ -0,0 +1,30 @@
1
+ // The page's side of `system`: Arcadible's state for the player, sent as
2
+ // the game connects and again whenever it changes. Nothing waits on it.
3
+ import { isLanguage, Names } from "./rules.js";
4
+ // [function]
5
+ export function createSystem(initial) {
6
+ let state = { ...initial };
7
+ let context = null;
8
+ return {
9
+ set(next) {
10
+ const language = next.language !== undefined && isLanguage(next.language)
11
+ ? next.language
12
+ : state.language;
13
+ if (language === state.language)
14
+ return;
15
+ state = { ...state, language };
16
+ context?.broadcast(Names.State, state);
17
+ },
18
+ module(ctx) {
19
+ context = ctx;
20
+ return {
21
+ handlers: {},
22
+ // the state now, first on each channel, so the game has it by
23
+ // `ready`.
24
+ async open(send) {
25
+ send(Names.State, state);
26
+ },
27
+ };
28
+ },
29
+ };
30
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.5.0-next.0";
1
+ export declare const VERSION = "1.5.0-next.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // The client's version, sent in its hello and shown as `arcadible.version`,
2
2
  // for bug reports. It matches package.json (test/unit/protocol.test.ts).
3
- export const VERSION = "1.5.0-next.0";
3
+ export const VERSION = "1.5.0-next.2";
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  ],
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "1.5.0-next.0"
46
+ "version": "1.5.0-next.2"
47
47
  }