@wyndlass/app-sdk 1.3.0 → 1.5.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/dist/react.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { type ComponentType } from "react";
10
10
  import type { DataAnswer, ScreenFacts } from "./contract/app-contract.js";
11
+ import { type Followed } from "./following.js";
11
12
  type Params = Record<string, string | number | boolean>;
12
13
  /**
13
14
  * An element class for a component, the module's default export: `export default reactApp(Board, { css })`. `css` is
@@ -60,4 +61,23 @@ export declare function useData<T = unknown>(source: string, params: Params, eve
60
61
  * every frame, as before.
61
62
  */
62
63
  export declare function useSnapshot(table: string): unknown | null;
64
+ /** The player's state: starting, playing, paused, stopped, idle or error. Null where the app has no controller (the designer). */
65
+ export declare function usePlayback(): Followed<"playback">;
66
+ /** Whether the screen is online or offline; null without a controller. */
67
+ export declare function useConnection(): Followed<"connection">;
68
+ /** The screen's volume and whether it is muted; null without a controller, or where the host sets no audio. */
69
+ export declare function useAudio(): Followed<"audio">;
70
+ /** The items playing (the status's `now`); null without a controller. */
71
+ export declare function usePlaying(): Followed<"now">;
72
+ /**
73
+ * Calls `handler` with the argument of each command of this name sent to the screen's apps (`apps.sendCommand`,
74
+ * WP-0150 decision 3), from mount until the component goes. The handler may change between renders: the latest is called.
75
+ */
76
+ export declare function useCommand(name: string, handler: (argument: Record<string, unknown> | undefined) => void): void;
77
+ /**
78
+ * Emergency messages at the screen (WP-0150 decision 5): what shows (`none`, `banner` or `takeover`) and each message's
79
+ * facts, never its words; the component renders when they change. Null without a controller, or before the screen has
80
+ * said what it holds. Under a banner the app plays on below it: it may lower its sound or pause what calls for attention.
81
+ */
82
+ export declare function useEmergency(): Followed<"emergency">;
63
83
  export {};
package/dist/react.js CHANGED
@@ -17,8 +17,10 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
17
17
  * React's tree (which hangs off the shadow root's nodes) holds nothing another app could use: data, images and the
18
18
  * rest go through the SDK's functions, as for any app.
19
19
  */
20
- import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
20
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
21
21
  import { createRoot } from "react-dom/client";
22
+ import { follow } from "./following.js";
23
+ import { onCommand } from "./commands.js";
22
24
  import { contextOf, data, kept, snapshot, WyndlassApp } from "./index.js";
23
25
  const bridges = new WeakMap();
24
26
  const App = createContext(null);
@@ -169,3 +171,33 @@ export function useSnapshot(table) {
169
171
  useSyncExternalStore(subscribe, read, read);
170
172
  return snapshot(app, table);
171
173
  }
174
+ /** A field of the player's status (WP-0150 decision 2): the component renders when its value changes, not at frames. */
175
+ function useFollowed(field) {
176
+ const [app] = useBridge();
177
+ const f = useMemo(() => follow(app, field), [app, field]);
178
+ return useSyncExternalStore(f.subscribe, f.get, f.get);
179
+ }
180
+ /** The player's state: starting, playing, paused, stopped, idle or error. Null where the app has no controller (the designer). */
181
+ export function usePlayback() { return useFollowed("playback"); }
182
+ /** Whether the screen is online or offline; null without a controller. */
183
+ export function useConnection() { return useFollowed("connection"); }
184
+ /** The screen's volume and whether it is muted; null without a controller, or where the host sets no audio. */
185
+ export function useAudio() { return useFollowed("audio"); }
186
+ /** The items playing (the status's `now`); null without a controller. */
187
+ export function usePlaying() { return useFollowed("now"); }
188
+ /**
189
+ * Calls `handler` with the argument of each command of this name sent to the screen's apps (`apps.sendCommand`,
190
+ * WP-0150 decision 3), from mount until the component goes. The handler may change between renders: the latest is called.
191
+ */
192
+ export function useCommand(name, handler) {
193
+ const [app] = useBridge();
194
+ const latest = useRef(handler);
195
+ latest.current = handler;
196
+ useEffect(() => onCommand(app, name, argument => latest.current(argument)), [app, name]);
197
+ }
198
+ /**
199
+ * Emergency messages at the screen (WP-0150 decision 5): what shows (`none`, `banner` or `takeover`) and each message's
200
+ * facts, never its words; the component renders when they change. Null without a controller, or before the screen has
201
+ * said what it holds. Under a banner the app plays on below it: it may lower its sound or pause what calls for attention.
202
+ */
203
+ export function useEmergency() { return useFollowed("emergency"); }
package/dist/signals.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Table rows as Preact signals (WP-0149 decision 5, `@wyndlass/app-sdk/signals`): for an app that updates its DOM when its
3
- * rows change, in an effect, rather than at every frame. An entry of its own, so an app that does not import it does not
4
- * bundle it. Its signals are this copy of `@preact/signals-core`'s: take effect, computed and the rest from here too, as
5
- * an effect from another copy does not see them.
2
+ * Table rows and the player's state as Preact signals (WP-0149 decision 5, WP-0150 decision 1;
3
+ * `@wyndlass/app-sdk/signals`): for an app that updates its DOM when they change, in an effect, rather than at every
4
+ * frame. An entry of its own, so an app that does not import it does not bundle it. Its signals are this copy of
5
+ * `@preact/signals-core`'s: take effect, computed and the rest from here too, as an effect from another copy does not
6
+ * see them.
6
7
  */
7
8
  import { type ReadonlySignal } from "@preact/signals-core";
9
+ import type { PlayerAPIStatus } from "./contract/player-api-status.js";
8
10
  import { type WyndlassApp } from "./index.js";
9
11
  export { batch, computed, effect, signal, untracked, type ReadonlySignal, type Signal } from "@preact/signals-core";
10
12
  /**
@@ -15,3 +17,25 @@ export { batch, computed, effect, signal, untracked, type ReadonlySignal, type S
15
17
  * On a player before WP-0149, which makes the app again when a table changes, it keeps the rows the app was made with.
16
18
  */
17
19
  export declare function tableSignal(app: WyndlassApp, table: string): ReadonlySignal<unknown | null>;
20
+ /**
21
+ * The player's state as a read-only signal: starting, playing, paused, stopped, idle or error, set on each
22
+ * `state.changed`. Null where the app has no controller (the designer). Make it in mount(), where the app has its context.
23
+ */
24
+ export declare function playbackSignal(app: WyndlassApp): ReadonlySignal<PlayerAPIStatus["state"] | null>;
25
+ /** Whether the screen is online or offline, as a read-only signal set on each `connection.changed`; null without a controller. */
26
+ export declare function connectionSignal(app: WyndlassApp): ReadonlySignal<PlayerAPIStatus["connection"] | null>;
27
+ /**
28
+ * The screen's volume and whether it is muted, as a read-only signal set on each `audio.changed`. Null without a
29
+ * controller, or on a screen whose host sets no audio.
30
+ */
31
+ export declare function audioSignal(app: WyndlassApp): ReadonlySignal<NonNullable<PlayerAPIStatus["audio"]> | null>;
32
+ /** The items playing (the status's `now`), as a read-only signal read again on each `item.start` and `item.end`; null without a controller. */
33
+ export declare function nowSignal(app: WyndlassApp): ReadonlySignal<PlayerAPIStatus["now"] | null>;
34
+ /**
35
+ * Emergency messages at the screen, as a read-only signal set on each `emergency.changed` (WP-0150 decision 5): what
36
+ * shows (`none`, `banner` or `takeover`), and of each message in force its source, severity, presentation, event,
37
+ * categories and whether it is a test. Never its words: the screen shows them, over the app. Null without a controller,
38
+ * or before the screen has said what it holds. Under a banner the app plays on below it: it may lower its sound, pause
39
+ * what calls for attention, or show the way out. It cannot move or cover the emergency layer.
40
+ */
41
+ export declare function emergencySignal(app: WyndlassApp): ReadonlySignal<NonNullable<PlayerAPIStatus["emergency"]> | null>;
package/dist/signals.js CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Table rows as Preact signals (WP-0149 decision 5, `@wyndlass/app-sdk/signals`): for an app that updates its DOM when its
3
- * rows change, in an effect, rather than at every frame. An entry of its own, so an app that does not import it does not
4
- * bundle it. Its signals are this copy of `@preact/signals-core`'s: take effect, computed and the rest from here too, as
5
- * an effect from another copy does not see them.
2
+ * Table rows and the player's state as Preact signals (WP-0149 decision 5, WP-0150 decision 1;
3
+ * `@wyndlass/app-sdk/signals`): for an app that updates its DOM when they change, in an effect, rather than at every
4
+ * frame. An entry of its own, so an app that does not import it does not bundle it. Its signals are this copy of
5
+ * `@preact/signals-core`'s: take effect, computed and the rest from here too, as an effect from another copy does not
6
+ * see them.
6
7
  */
7
8
  import { computed, signal } from "@preact/signals-core";
9
+ import { follow } from "./following.js";
8
10
  import { eachFrame } from "./frames.js";
9
11
  import { contextOf, snapshot } from "./index.js";
10
12
  export { batch, computed, effect, signal, untracked } from "@preact/signals-core";
@@ -28,3 +30,42 @@ export function tableSignal(app, table) {
28
30
  }
29
31
  return computed(() => rows.value);
30
32
  }
33
+ /** A field of the player's status as a read-only signal (WP-0150 decision 1): set only when its value changes. */
34
+ function followed(app, field) {
35
+ const f = follow(app, field);
36
+ const value = signal(f.get());
37
+ f.subscribe(() => { value.value = f.get(); });
38
+ return computed(() => value.value);
39
+ }
40
+ /**
41
+ * The player's state as a read-only signal: starting, playing, paused, stopped, idle or error, set on each
42
+ * `state.changed`. Null where the app has no controller (the designer). Make it in mount(), where the app has its context.
43
+ */
44
+ export function playbackSignal(app) {
45
+ return followed(app, "playback");
46
+ }
47
+ /** Whether the screen is online or offline, as a read-only signal set on each `connection.changed`; null without a controller. */
48
+ export function connectionSignal(app) {
49
+ return followed(app, "connection");
50
+ }
51
+ /**
52
+ * The screen's volume and whether it is muted, as a read-only signal set on each `audio.changed`. Null without a
53
+ * controller, or on a screen whose host sets no audio.
54
+ */
55
+ export function audioSignal(app) {
56
+ return followed(app, "audio");
57
+ }
58
+ /** The items playing (the status's `now`), as a read-only signal read again on each `item.start` and `item.end`; null without a controller. */
59
+ export function nowSignal(app) {
60
+ return followed(app, "now");
61
+ }
62
+ /**
63
+ * Emergency messages at the screen, as a read-only signal set on each `emergency.changed` (WP-0150 decision 5): what
64
+ * shows (`none`, `banner` or `takeover`), and of each message in force its source, severity, presentation, event,
65
+ * categories and whether it is a test. Never its words: the screen shows them, over the app. Null without a controller,
66
+ * or before the screen has said what it holds. Under a banner the app plays on below it: it may lower its sound, pause
67
+ * what calls for attention, or show the way out. It cannot move or cover the emergency layer.
68
+ */
69
+ export function emergencySignal(app) {
70
+ return followed(app, "emergency");
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyndlass/app-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Build apps for Wyndlass's screens: web components on the app contract (Player API 2), with the runtime Wyndlass's own apps use.",
5
5
  "keywords": [
6
6
  "wyndlass",
@@ -28,7 +28,8 @@ A paged app also defines `paging()`, returning `{count, ms}`, so the layout can
28
28
  - `kept(this, source, params)`: the answer the screen last kept for the same request, at once, or `undefined`. Draw it while `data` asks again, so the app is ready without the network;
29
29
  - `image(this, path)`: a picture a data answer names, as a URL the app can draw;
30
30
  - `snapshot(this, table)`: the rows of a table the layout names, in a `datatable` param or in the element's `binding`. Where a binding chooses rows (a filter, a sort, a limit), only those. `null` when the screen has no snapshot of that table. A `datatable` param needs no binding. A screen gives the app a table's new rows while it plays: read them where you draw (in `draw()`, `useSnapshot` or `tableSignal`), never once in `mount()`. They are the same object until they change, and frozen: copy rows before you sort or change them;
31
- - `controllerOf(this)`: the player's controller, as `capabilities.player` allows; absent in the designer and previews;
31
+ - `controllerOf(this)`: the player's controller, as `capabilities.player` allows; absent in the designer and the CMS's previews (`wyndlass-app preview` gives a stand-in). Follow its state with the signals or hooks below, never by reading `status()` at each frame;
32
+ - `onCommand(this, name, handler)`: each command of that name sent to the screen's apps (`apps.sendCommand`), with its argument; returns how to stop. A command is an event: the same one sent twice runs twice. Apps cannot answer one;
32
33
  - `impressionsOf(this)`: the people at the screen, for an app whose definition declares the `impressions` sensor;
33
34
  - `contextOf(this)`: the rest of the context (the screen's `place` and facts, `preview`).
34
35
 
@@ -43,16 +44,29 @@ A paged app also defines `paging()`, returning `{count, ms}`, so the layout can
43
44
  - `useNow(everyMs?)`: the layout's clock. Without `everyMs`, drawn again at each of its frames, so the component renders many times a second. With it, the time is a multiple of `everyMs` (`useNow(1000)`: whole seconds), and the component renders only when that changes: take the coarsest the app shows. Either way, make formatters and other objects with `useMemo`, never in the render;
44
45
  - `usePaging(count, ms)`: the page showing now, rendering when it turns. It also tells the host the pages;
45
46
  - `useData(source, params)`: `{answer, error}`. The kept answer at once, then the service's, asked again every 10 minutes;
46
- - `useSnapshot(table)`: a data table's rows, as `snapshot` gives them. The component renders when they change.
47
+ - `useSnapshot(table)`: a data table's rows, as `snapshot` gives them. The component renders when they change;
48
+ - `usePlayback()`, `useConnection()`, `useAudio()`, `usePlaying()`: the player's state (starting, playing, paused, stopped, idle, error), online or offline, `{volume, muted}`, and the items playing. Each renders only when its value changes; `null` without a controller;
49
+ - `useCommand(name, handler)`: each command of that name, with its argument, while the component is mounted;
50
+ - `useEmergency()`: emergency messages at the screen (below).
47
51
 
48
52
  ## Signals (`@wyndlass/app-sdk/signals`)
49
53
 
50
- For a vanilla app that updates its DOM when its rows change, rather than at every frame:
54
+ For a vanilla app that updates its DOM when its rows or the player's state change, rather than at every frame:
51
55
  - `tableSignal(this, table)`: a read-only Preact signal of the table's snapshot, made in `mount()`. Its value changes when the rows do;
56
+ - `playbackSignal(this)`, `connectionSignal(this)`, `audioSignal(this)`, `nowSignal(this)`: the player's state, online or offline, `{volume, muted}`, and the items playing, as read-only signals made in `mount()`. Each is set only when its value changes; `null` without a controller;
52
57
  - `effect`, `computed`, `batch`, `signal`, `untracked`: Preact's, from the same copy. Import them from here, never from `@preact/signals-core`, whose effects would not see these signals.
53
58
 
59
+ - `emergencySignal(this)`: emergency messages at the screen (below), as a read-only signal.
60
+
54
61
  Draw in an `effect(() => { … rows.value … })` made in `mount()`, and leave `draw()` empty.
55
62
 
63
+ ## Emergency messages
64
+
65
+ The screen shows them itself, over every app: a takeover over all of it, a banner over its bottom 14% while the app plays on above. `emergencySignal(this)` and `useEmergency()` give `{ showing, messages }`: `showing` is `none`, `banner` or `takeover`, and each message gives `source`, `severity`, `presentation`, `event` and `category` where it has them, and `test`. `null` without a controller or before the screen has said what it holds, which is not `none`.
66
+ - **Under a banner,** lower the app's sound, stop calling numbers or pause what calls for attention, or show the way out.
67
+ - **Never draw an alert's words,** and never try to move, cover or hide the emergency layer: the screen shows the words in one design, and the layer stays on top.
68
+ - **Try it in the preview:** the controller's panel shows a Test banner or takeover over the app.
69
+
56
70
  ## The definition, `app.json`
57
71
 
58
72
  Valid against `node_modules/@wyndlass/app-sdk/protocol/app-definition.schema.json`.
@@ -6,6 +6,8 @@ An app for Wyndlass's screens, on `@wyndlass/app-sdk` and React. Its README says
6
6
  - **Time:** `useNow()` gives the layout's clock, drawn again at each of its frames, and `useNow(1000)` whole seconds, rendering once a second: take the coarsest the app shows; never `Date.now()`, so a seek or a pause shows the right moment.
7
7
  - **Nothing made in a render:** the component renders at every frame, many times a second. Make formatters (`Intl.DateTimeFormat`, `Intl.NumberFormat`) and other objects with `useMemo` on what they depend on, never in the render. One made in each draw holds memory a screen does not free in time: an app that did so ran a 4 GB screen out of memory within an hour.
8
8
  - **A table's rows from `useSnapshot(table)`:** a screen gives the app a table's new rows while it plays, and the component renders when they change. Never keep a copy made once. They are frozen: copy before you sort or change them.
9
+ - **The player's state** (playing or paused, online or offline, its sound): `usePlayback()`, `useConnection()` and `useAudio()`, which render only when it changes. **Commands** sent to the screen's apps: `useCommand(name, handler)`. A command is an event, and the app cannot answer it.
10
+ - **Emergency messages:** the screen shows them over the app itself. `useEmergency()` says what shows: under a banner the app plays on, so lower its sound or pause what calls for attention. Never draw an alert's words, and never try to move, cover or hide the emergency layer. Try it in the preview's controller panel.
9
11
  - **Ready:** call `useApp().ready()` once drawn, within 5 s, or the screen reports the app late. `useData()` gives the answer the screen kept at once, then the service's.
10
12
  - **Data:** only through `useData(source, params)`, and only the sources `app.json`'s `data` lists. Pages: `usePaging(count, ms)`.
11
13
  - **Styles:** `src/app.css`, passed as `css`, never a `<style>`.
@@ -6,6 +6,8 @@ An app for Wyndlass's screens, on `@wyndlass/app-sdk`. Its README says what it i
6
6
  - **Time:** from `now()` or `draw`'s `epochMs`, never `Date.now()`, so a seek or a pause shows the right moment.
7
7
  - **Nothing made in `draw`:** it runs at every frame, many times a second. Make formatters (`Intl.DateTimeFormat`, `Intl.NumberFormat`) and other objects in `mount()`, never in `draw`. One made in each draw holds memory a screen does not free in time: an app that did so ran a 4 GB screen out of memory within an hour.
8
8
  - **A table's rows where you draw:** a screen gives the app a table's new rows while it plays. Read them with `snapshot(this, table)` in `draw`, or with `tableSignal(this, table)` and an `effect` from `@wyndlass/app-sdk/signals`, made in `mount()`. Never read them once in `mount()` and keep them. They are frozen: copy before you sort or change them.
9
+ - **The player's state** (playing or paused, online or offline, its sound): `playbackSignal(this)`, `connectionSignal(this)` and `audioSignal(this)` from `@wyndlass/app-sdk/signals`, made in `mount()`, never `status()` read at each frame. **Commands** sent to the screen's apps: `onCommand(this, name, handler)`. A command is an event, and the app cannot answer it.
10
+ - **Emergency messages:** the screen shows them over the app itself. `emergencySignal(this)` says what shows: under a banner the app plays on, so lower its sound or pause what calls for attention. Never draw an alert's words, and never try to move, cover or hide the emergency layer. Try it in the preview's controller panel.
9
11
  - **Ready:** call `ready()` once drawn, within 5 s, or the screen reports the app late. Draw the answer `kept()` gives at once, then ask `data()`.
10
12
  - **Data:** only through `data(this, source, params)`, and only the sources `app.json`'s `data` lists. Images a data answer names through `image(this, path)`.
11
13
  - **Styles:** `this.css(text)`, never a `<style>`. A CSS file the source imports is its text.