@arcadible/sdk 1.5.0-next.1 → 1.5.0-next.3

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,5 @@
1
1
  import { type Build } from "./bus/protocol.js";
2
+ import { type Context } from "./context/client.js";
2
3
  import { type System } from "./system/client.js";
3
4
  import type { Backend } from "./saves/backend/types.js";
4
5
  import { type Saves } from "./saves/client.js";
@@ -31,6 +32,13 @@ export interface Arcadible {
31
32
  * `undefined` before.
32
33
  */
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;
34
42
  /**
35
43
  * Starts the game's initialization: call it as your code starts, before
36
44
  * anything it awaits, and Arcadible shows the game's loading screen until
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ 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";
9
10
  import { createSystemClient } from "./system/client.js";
10
11
  import { indexedDBBackend } from "./saves/backend/indexeddb.js";
11
12
  import { createSaves } from "./saves/client.js";
@@ -37,9 +38,11 @@ export function createClient(options = {}) {
37
38
  });
38
39
  let settled = null;
39
40
  const system = createSystemClient(bus);
41
+ const context = createContextClient(bus);
40
42
  // ready once welcomed, and each module's first messages have arrived.
41
43
  const ready = Promise.all([bus.welcomed, opened]).then(([welcome]) => {
42
44
  system.settle();
45
+ context.settle();
43
46
  settled = { build: buildOf(welcome.build) };
44
47
  return settled;
45
48
  });
@@ -55,6 +58,9 @@ export function createClient(options = {}) {
55
58
  get system() {
56
59
  return system.value;
57
60
  },
61
+ get context() {
62
+ return context.value;
63
+ },
58
64
  initStart() {
59
65
  lifecycle.start();
60
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, and none comes after `ready`.
12
+ if (context || settled)
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,5 +1,6 @@
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";
4
5
  import { type SystemState } from "./system/rules.js";
5
6
  import { type System } from "./system/server.js";
@@ -16,6 +17,7 @@ export type ServeOptions = {
16
17
  lifecycle?: LifecycleHost;
17
18
  holds?: Holds;
18
19
  system?: System;
20
+ context?: ContextState;
19
21
  };
20
22
  export type Host = {
21
23
  close(): void;
@@ -23,8 +25,9 @@ export type Host = {
23
25
  leave(timeout?: number): Promise<void>;
24
26
  setSystem(state: Partial<SystemState>): void;
25
27
  };
26
- type HostOptions = Omit<ServeOptions, "holds" | "system"> & {
28
+ type HostOptions = Omit<ServeOptions, "holds" | "system" | "context"> & {
27
29
  language?: string;
30
+ launchedAt?: string;
28
31
  frame: HTMLIFrameElement;
29
32
  origin: string;
30
33
  window?: Window;
package/dist/host.js CHANGED
@@ -1,6 +1,8 @@
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";
5
+ import { isTime } from "./context/rules.js";
4
6
  import { createHolds } from "./lifecycle/holds.js";
5
7
  import { shortLanguage } from "./system/rules.js";
6
8
  import { createSystem } from "./system/server.js";
@@ -17,6 +19,7 @@ export function serve(options) {
17
19
  // state, by `ready`.
18
20
  ...(options.holds ? [options.holds.module] : []),
19
21
  ...(options.system ? [options.system.module] : []),
22
+ ...(options.context ? [contextServer(options.context)] : []),
20
23
  savesServer({
21
24
  backend,
22
25
  now: options.now ?? (() => new Date()),
@@ -43,7 +46,20 @@ export function createHost(options) {
43
46
  const answering = answerHellos({
44
47
  frame: options.frame,
45
48
  origin: options.origin,
46
- server: serve({ ...options, holds, system }),
49
+ server: serve({
50
+ ...options,
51
+ holds,
52
+ system,
53
+ context: {
54
+ // Arcadible's time when given, else the device's: never a time the
55
+ // game would refuse.
56
+ launch: {
57
+ at: isTime(options.launchedAt)
58
+ ? options.launchedAt
59
+ : new Date().toISOString(),
60
+ },
61
+ },
62
+ }),
47
63
  window: win,
48
64
  onHello: options.onHello,
49
65
  });
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ 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
7
  export type { System } from "./system/client.js";
8
+ export type { Context } from "./context/client.js";
8
9
  export type { SaveInfo } from "./saves/rules.js";
@@ -58,7 +58,10 @@ export function createHolds() {
58
58
  },
59
59
  [Names.Paused]: (data) => {
60
60
  const value = isObject(data) ? data["n"] : undefined;
61
- if (typeof value !== "number" || value > n)
61
+ // a pause the page sent: a whole number, from 1 up to the last.
62
+ if (!Number.isInteger(value) || value < 1)
63
+ return;
64
+ if (value > n)
62
65
  return;
63
66
  settled = Math.max(settled, value);
64
67
  if (settled < n)
@@ -8,5 +8,7 @@ export type SystemState = {
8
8
  export declare function isLanguage(value: unknown): value is string;
9
9
  /** The state as the game keeps it: only fields it knows, each checked. */
10
10
  export declare function stateOf(value: unknown): SystemState | null;
11
+ /** A language tag as a short one: `en-US` as `en`, or null if it isn't one. */
12
+ export declare function languageOf(tag: unknown): string | null;
11
13
  /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
12
14
  export declare function shortLanguage(tag: string | undefined): string;
@@ -23,8 +23,15 @@ export function stateOf(value) {
23
23
  return { language: value["language"] };
24
24
  }
25
25
  // [function]
26
+ /** A language tag as a short one: `en-US` as `en`, or null if it isn't one. */
27
+ export function languageOf(tag) {
28
+ if (typeof tag !== "string")
29
+ return null;
30
+ const short = tag.split("-")[0]?.toLowerCase() ?? "";
31
+ return isLanguage(short) ? short : null;
32
+ }
33
+ // [function]
26
34
  /** A browser's language as a short tag: `en-US` as `en`, or `en`. */
27
35
  export function shortLanguage(tag) {
28
- const short = (tag ?? "").split("-")[0]?.toLowerCase() ?? "";
29
- return isLanguage(short) ? short : "en";
36
+ return languageOf(tag) ?? "en";
30
37
  }
@@ -1,15 +1,15 @@
1
1
  // The page's side of `system`: Arcadible's state for the player, sent as
2
2
  // the game connects and again whenever it changes. Nothing waits on it.
3
- import { isLanguage, Names } from "./rules.js";
3
+ import { languageOf, Names, shortLanguage } from "./rules.js";
4
4
  // [function]
5
5
  export function createSystem(initial) {
6
- let state = { ...initial };
6
+ // only a short tag goes to the game: `en-US` as `en`.
7
+ let state = { ...initial, language: shortLanguage(initial.language) };
7
8
  let context = null;
8
9
  return {
9
10
  set(next) {
10
- const language = next.language !== undefined && isLanguage(next.language)
11
- ? next.language
12
- : state.language;
11
+ // a tag that isn't a language changes nothing.
12
+ const language = languageOf(next.language) ?? state.language;
13
13
  if (language === state.language)
14
14
  return;
15
15
  state = { ...state, language };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.5.0-next.1";
1
+ export declare const VERSION = "1.5.0-next.3";
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.1";
3
+ export const VERSION = "1.5.0-next.3";
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.1"
46
+ "version": "1.5.0-next.3"
47
47
  }