fpv-laptimer-client 0.0.0-stage → 0.0.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/src/types.js ADDED
@@ -0,0 +1,276 @@
1
+ // @ts-check
2
+ // The JSON the server sends (the laptimer's docs/api/datenformate.md and events.md).
3
+ // Only typedefs: this file exports nothing at runtime.
4
+
5
+ /**
6
+ * A channel (a seat on the page: slot 1 = index 0).
7
+ * @typedef {object} Channel
8
+ * @property {number} index 0, 1, …
9
+ * @property {string} name "R1" … "R8" or the MHz as text
10
+ * @property {number} mhz
11
+ * @property {boolean} enabled false: does not fly along
12
+ * @property {number | null} pilot_id
13
+ * @property {string | null} pilot callsign of the assigned pilot
14
+ * @property {string} label free text instead of a pilot (wins over `pilot`)
15
+ * @property {string} shown what the page shows
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} Pilot
20
+ * @property {number} id
21
+ * @property {string} name callsign (unique)
22
+ * @property {string} full_name may be empty
23
+ */
24
+
25
+ /**
26
+ * One seat's result in a session (`results`, in seat order).
27
+ * @typedef {object} SeatResult
28
+ * @property {number} channel
29
+ * @property {string} channel_name
30
+ * @property {number} mhz
31
+ * @property {number | null} pilot_id
32
+ * @property {string} pilot shown name: callsign, free text or channel name
33
+ * @property {number | null} position null without a lap
34
+ * @property {number} laps
35
+ * @property {number[]} lap_times_us
36
+ * @property {number | null} last_lap_us
37
+ * @property {number | null} best_lap_us
38
+ * @property {number | null} best_consecutive_us
39
+ * @property {number | null} total_us
40
+ * @property {number | null} holeshot_us
41
+ * @property {boolean} finished
42
+ */
43
+
44
+ /**
45
+ * A pass through the gate (only in `sessions.get`).
46
+ * @typedef {object} Pass
47
+ * @property {number} id
48
+ * @property {number} channel
49
+ * @property {number} time_us
50
+ * @property {number} peak_db
51
+ * @property {boolean} manual added by hand
52
+ * @property {boolean} deleted
53
+ * @property {number} gate 0 = finish, 1 = arming gate
54
+ * @property {'no_arming' | null} [rejected] deleted by the server, and why
55
+ * @property {number | null} [detected_us] where the detector put it (null: added by hand; differs from time_us: moved by hand)
56
+ */
57
+
58
+ /**
59
+ * A correction of a pass (only in `sessions.get`, as `changes`).
60
+ * @typedef {object} PassChange
61
+ * @property {number | null} pass_id
62
+ * @property {number} channel
63
+ * @property {'add' | 'delete' | 'restore' | 'move' | 'reject'} action
64
+ * @property {number | null} from_us
65
+ * @property {number | null} to_us
66
+ * @property {string | null} rejected
67
+ * @property {boolean} live while the session ran
68
+ * @property {'seite' | 'editor' | 'api' | 'server'} origin
69
+ * @property {number} at_us
70
+ */
71
+
72
+ /** @typedef {'holeshot' | 'from_start' | 'flying'} StartMode */
73
+
74
+ /**
75
+ * @typedef {object} Session
76
+ * @property {number} id
77
+ * @property {boolean} race false = training
78
+ * @property {'countdown' | 'running' | 'ended'} phase
79
+ * @property {StartMode} start_mode
80
+ * @property {'open' | 'laps' | 'seconds'} end
81
+ * @property {number | null} end_value
82
+ * @property {number} countdown_s
83
+ * @property {number} min_lap_s
84
+ * @property {number} created_us
85
+ * @property {number} start_us
86
+ * @property {number | null} ended_us
87
+ * @property {number | null} confirmed_us
88
+ * @property {boolean} needs_confirmation
89
+ * @property {SeatResult[]} results
90
+ * @property {Pass[]} [passes] only from `sessions.get`
91
+ * @property {PassChange[]} [changes] only from `sessions.get`: every correction of a pass
92
+ */
93
+
94
+ /**
95
+ * @typedef {object} Board
96
+ * @property {string} id
97
+ * @property {string} port
98
+ * @property {string} role finish, arming or off
99
+ * @property {boolean} ok
100
+ * @property {string | null} error
101
+ * @property {number} levels_per_s
102
+ */
103
+
104
+ /**
105
+ * @typedef {object} State
106
+ * @property {number} now_us
107
+ * @property {Session | null} session the running session
108
+ * @property {Session | null} unconfirmed the ended race waiting for confirmation
109
+ * @property {Channel[]} channels
110
+ * @property {Pilot[]} pilots
111
+ * @property {Board[]} boards
112
+ */
113
+
114
+ /**
115
+ * @typedef {object} PilotStats
116
+ * @property {number | null} pilot_id
117
+ * @property {string} label
118
+ * @property {number | null} best_lap_us
119
+ * @property {number | null} best_consecutive_us
120
+ * @property {number} sessions
121
+ * @property {number} races
122
+ * @property {number} wins
123
+ */
124
+
125
+ /**
126
+ * @typedef {object} Stats
127
+ * @property {number} today_races
128
+ * @property {PilotStats[]} all best lap first
129
+ * @property {PilotStats[]} today
130
+ */
131
+
132
+ /**
133
+ * Arguments of `session.start`.
134
+ * @typedef {object} StartOptions
135
+ * @property {boolean} [race] true: race, false: training (default)
136
+ * @property {number} [laps] race: end after this many laps (1–100)
137
+ * @property {number} [seconds] race: end after this many seconds (10–3600), instead of laps
138
+ * @property {number} [countdown_s] race: 0–60
139
+ * @property {StartMode} [start_mode] race
140
+ */
141
+
142
+ /**
143
+ * Arguments of `channels.set` (only given fields change).
144
+ * @typedef {object} ChannelChange
145
+ * @property {string | null} [pilot] callsign, null = nobody
146
+ * @property {number | null} [pilot_id] instead of `pilot`
147
+ * @property {boolean} [create] make an unknown callsign a pilot
148
+ * @property {string} [label] free text instead of a pilot ("" = none)
149
+ * @property {boolean} [enabled]
150
+ * @property {string | number} [frequency] "R1" … "R8" or MHz (5300–6000); not during a session
151
+ */
152
+
153
+ // --- events ---
154
+
155
+ /**
156
+ * Fields every event about a pilot has ("Sitz" in docs/api/events.md).
157
+ * @typedef {object} Seat
158
+ * @property {number} channel
159
+ * @property {string} channel_name
160
+ * @property {number} mhz
161
+ * @property {number | null} pilot_id
162
+ * @property {string} pilot
163
+ */
164
+
165
+ /**
166
+ * @typedef {object} SessionStartedData
167
+ * @property {number} session_id
168
+ * @property {boolean} race
169
+ * @property {StartMode} start_mode
170
+ * @property {'open' | 'laps' | 'seconds'} end
171
+ * @property {number | null} end_value
172
+ * @property {number} countdown_s
173
+ * @property {number} created_us
174
+ * @property {number} start_us
175
+ * @property {number} pilot_count
176
+ * @property {Seat[]} seats
177
+ */
178
+
179
+ /**
180
+ * @typedef {object} CountdownData
181
+ * @property {number} session_id
182
+ * @property {boolean} race
183
+ * @property {number} seconds_left
184
+ */
185
+
186
+ /**
187
+ * @typedef {object} StartSignalData
188
+ * @property {number} session_id
189
+ * @property {boolean} race
190
+ * @property {number} start_us
191
+ */
192
+
193
+ /**
194
+ * @typedef {Seat & {
195
+ * session_id: number, race: boolean, pass_id: number, time_us: number, manual: boolean,
196
+ * kind: 'lap' | 'holeshot' | 'start' | 'ignored',
197
+ * lap: number | null, lap_time_us: number | null, lap_time_s: number | null, laps: number,
198
+ * best_lap_us: number | null, best_lap_s: number | null, personal_best: boolean, session_best: boolean,
199
+ * holeshot_us: number | null, finished: boolean, position: number | null
200
+ * }} PilotPassData
201
+ */
202
+
203
+ /**
204
+ * @typedef {Seat & {
205
+ * session_id: number, laps: number, total_us: number | null, total_s: number | null,
206
+ * best_lap_us: number | null, best_lap_s: number | null, position: number | null
207
+ * }} PilotFinishedData
208
+ */
209
+
210
+ /**
211
+ * @typedef {object} SessionStoppedData
212
+ * @property {number} session_id
213
+ * @property {boolean} race
214
+ * @property {'stopped' | 'finished' | 'cancelled'} reason
215
+ * @property {number} ended_us
216
+ * @property {boolean} needs_confirmation
217
+ * @property {string | null} winner race only
218
+ * @property {SeatResult[]} results
219
+ */
220
+
221
+ /**
222
+ * @typedef {object} RaceConfirmedData
223
+ * @property {number} session_id
224
+ * @property {boolean} race
225
+ * @property {number} confirmed_us
226
+ * @property {string | null} winner
227
+ * @property {SeatResult[]} results
228
+ */
229
+
230
+ /**
231
+ * @typedef {object} PilotEventData
232
+ * @property {number} pilot_id
233
+ * @property {string} name
234
+ * @property {string} [full_name] not in pilot_deleted
235
+ */
236
+
237
+ /**
238
+ * Event name → its `data`.
239
+ * @typedef {object} EventMap
240
+ * @property {SessionStartedData} session_started
241
+ * @property {CountdownData} countdown
242
+ * @property {StartSignalData} start_signal
243
+ * @property {PilotPassData} pilot_pass
244
+ * @property {PilotFinishedData} pilot_finished
245
+ * @property {SessionStoppedData} session_stopped
246
+ * @property {RaceConfirmedData} race_confirmed
247
+ * @property {{session_id: number, race: boolean}} session_deleted
248
+ * @property {{deleted: number}} sessions_reset
249
+ * @property {{session_id: number}} passes_changed
250
+ * @property {{channels: Channel[]}} channels_changed
251
+ * @property {PilotEventData} pilot_added
252
+ * @property {PilotEventData} pilot_updated
253
+ * @property {PilotEventData} pilot_deleted
254
+ */
255
+
256
+ /**
257
+ * An event as it comes over the WebSocket.
258
+ * @template [D=any]
259
+ * @typedef {object} EventMessage
260
+ * @property {'event'} type
261
+ * @property {string} event
262
+ * @property {number} seq counts from 1 since the server started; a gap = missed events
263
+ * @property {number} time_us
264
+ * @property {D} data
265
+ */
266
+
267
+ /**
268
+ * The first message after connecting.
269
+ * @typedef {object} Hello
270
+ * @property {'hello'} type
271
+ * @property {number} api
272
+ * @property {State | null} state
273
+ * @property {string[]} commands
274
+ */
275
+
276
+ export {}
@@ -0,0 +1,13 @@
1
+ export type Channel = import("./types.js").Channel;
2
+ export type ChannelChange = import("./types.js").ChannelChange;
3
+ export type Pilot = import("./types.js").Pilot;
4
+ export type Session = import("./types.js").Session;
5
+ export type SeatResult = import("./types.js").SeatResult;
6
+ export type Pass = import("./types.js").Pass;
7
+ export type State = import("./types.js").State;
8
+ export type Stats = import("./types.js").Stats;
9
+ export type StartOptions = import("./types.js").StartOptions;
10
+ export type EventMap = import("./types.js").EventMap;
11
+ export type PilotPassData = import("./types.js").PilotPassData;
12
+ export type SessionStoppedData = import("./types.js").SessionStoppedData;
13
+ export { Laptimer, LaptimerError } from "./laptimer.js";
@@ -0,0 +1,253 @@
1
+ /** @import { Channel, ChannelChange, EventMap, EventMessage, Hello, Pilot, Session, StartOptions, State, Stats } from './types.js' */
2
+ /**
3
+ * An error from the server (its message is German, meant to be shown as is)
4
+ * or from the connection.
5
+ */
6
+ export class LaptimerError extends Error {
7
+ /**
8
+ * @param {string} message
9
+ * @param {object} [info]
10
+ * @param {number | null} [info.status] HTTP status (400 bad arguments, 401 key, 404 unknown command), the same over the WebSocket; null without an answer
11
+ * @param {string} [info.command]
12
+ * @param {unknown} [info.cause]
13
+ */
14
+ constructor(message: string, { status, command, cause }?: {
15
+ status?: number | null | undefined;
16
+ command?: string | undefined;
17
+ cause?: unknown;
18
+ });
19
+ /** @type {number | null} */
20
+ status: number | null;
21
+ /** @type {string | undefined} */
22
+ command: string | undefined;
23
+ }
24
+ /**
25
+ * What `on` can listen to: the server's events, `'*'` (every event) and
26
+ * three of the client's own: `hello` (connected, with the state),
27
+ * `lagged` (events were lost: fetch the state again) and `disconnected`.
28
+ * @typedef {EventMap & {
29
+ * '*': any,
30
+ * hello: Hello,
31
+ * lagged: {missed: number},
32
+ * disconnected: {code: number, reason: string},
33
+ * }} ClientEvents
34
+ */
35
+ /**
36
+ * @template {string} K
37
+ * @typedef {(data: K extends keyof ClientEvents ? ClientEvents[K] : any, msg: EventMessage<any> | any) => void} Listener
38
+ */
39
+ /**
40
+ * @typedef {object} LaptimerOptions
41
+ * @property {string} url e.g. "http://pi:5800"
42
+ * @property {string} key API key "lt_…"
43
+ * @property {'auto' | 'http'} [transport] auto (default): commands over the WebSocket while it is connected, else HTTP
44
+ * @property {number} [timeout_ms] per command, default 15000
45
+ * @property {number} [max_backoff_ms] longest wait between reconnects, default 10000
46
+ */
47
+ export class Laptimer {
48
+ /** @param {LaptimerOptions} options */
49
+ constructor({ url, key, transport, timeout_ms, max_backoff_ms }: LaptimerOptions);
50
+ url: string;
51
+ key: string;
52
+ transport: "auto" | "http";
53
+ timeout_ms: number;
54
+ max_backoff_ms: number;
55
+ channels: {
56
+ /** @returns {Promise<Channel[]>} */
57
+ list: () => Promise<Channel[]>;
58
+ /**
59
+ * One channel; only the given fields change. A pilot or frequency another
60
+ * channel has is swapped with it.
61
+ * @param {number} index 0 = first channel
62
+ * @param {ChannelChange} change
63
+ * @returns {Promise<Channel>}
64
+ */
65
+ set: (index: number, change: ChannelChange) => Promise<Channel>;
66
+ /**
67
+ * All channels at once, in order: callsign, pilot id or null per channel.
68
+ * @param {(string | number | null)[]} pilots
69
+ * @param {{create?: boolean}} [options] create: make unknown callsigns pilots
70
+ * @returns {Promise<Channel[]>}
71
+ */
72
+ assign: (pilots: (string | number | null)[], { create }?: {
73
+ create?: boolean;
74
+ }) => Promise<Channel[]>;
75
+ };
76
+ pilots: {
77
+ /** @returns {Promise<Pilot[]>} */
78
+ list: () => Promise<Pilot[]>;
79
+ /** @param {number} id @returns {Promise<Pilot>} */
80
+ get: (id: number) => Promise<Pilot>;
81
+ /** @param {string} name callsign @param {string} [full_name] @returns {Promise<Pilot>} */
82
+ create: (name: string, full_name?: string) => Promise<Pilot>;
83
+ /** @param {number} id @param {{name?: string, full_name?: string}} change @returns {Promise<Pilot>} */
84
+ update: (id: number, change: {
85
+ name?: string;
86
+ full_name?: string;
87
+ }) => Promise<Pilot>;
88
+ /** @param {number} id @returns {Promise<{}>} */
89
+ delete: (id: number) => Promise<{}>;
90
+ };
91
+ session: {
92
+ /**
93
+ * Starts a training (default) or a race. Refused while a session runs or
94
+ * a race waits for confirmation.
95
+ * @param {StartOptions} [options]
96
+ * @returns {Promise<Session>}
97
+ */
98
+ start: (options?: StartOptions) => Promise<Session>;
99
+ /** @returns {Promise<{session_id: number}>} */
100
+ stop: () => Promise<{
101
+ session_id: number;
102
+ }>;
103
+ /** Stops and deletes the running session. @returns {Promise<{session_id: number}>} */
104
+ cancel: () => Promise<{
105
+ session_id: number;
106
+ }>;
107
+ /** @param {number} [id] default: the race waiting for confirmation @returns {Promise<{session_id: number}>} */
108
+ confirm: (id?: number) => Promise<{
109
+ session_id: number;
110
+ }>;
111
+ /** Deletes an ended session. @param {number} [id] default: the race waiting for confirmation @returns {Promise<{session_id: number}>} */
112
+ discard: (id?: number) => Promise<{
113
+ session_id: number;
114
+ }>;
115
+ };
116
+ sessions: {
117
+ /** @param {{limit?: number}} [options] default 20 @returns {Promise<Session[]>} */
118
+ list: ({ limit }?: {
119
+ limit?: number;
120
+ }) => Promise<Session[]>;
121
+ /** With every pass (`passes`). @param {number} id @returns {Promise<Session>} */
122
+ get: (id: number) => Promise<Session>;
123
+ };
124
+ passes: {
125
+ /**
126
+ * A pass by hand: counts like a detected one (announcement, `pilot_pass`).
127
+ * @param {number} channel index
128
+ * @param {{time_us?: number, session_id?: number}} [options] default: now, the running session
129
+ * @returns {Promise<{pass_id: number, session_id: number, time_us: number}>}
130
+ */
131
+ add: (channel: number, { time_us, session_id }?: {
132
+ time_us?: number;
133
+ session_id?: number;
134
+ }) => Promise<{
135
+ pass_id: number;
136
+ session_id: number;
137
+ time_us: number;
138
+ }>;
139
+ /** Detected passes are only marked (restorable). @param {number} id @returns {Promise<{session_id: number}>} */
140
+ delete: (id: number) => Promise<{
141
+ session_id: number;
142
+ }>;
143
+ /** @param {number} id @returns {Promise<{session_id: number}>} */
144
+ restore: (id: number) => Promise<{
145
+ session_id: number;
146
+ }>;
147
+ };
148
+ /** @returns {Promise<{pong: true, now_us: number}>} */
149
+ ping(): Promise<{
150
+ pong: true;
151
+ now_us: number;
152
+ }>;
153
+ /** @returns {Promise<State>} */
154
+ state(): Promise<State>;
155
+ /** @returns {Promise<Stats>} */
156
+ stats(): Promise<Stats>;
157
+ /** True while the WebSocket is connected (and commands go over it with transport "auto"). */
158
+ get connected(): boolean;
159
+ /**
160
+ * Any command by name, e.g. `command('session.start', {race: true})`.
161
+ * Arguments that are `undefined` are left out.
162
+ * @param {string} name
163
+ * @param {Record<string, unknown>} [args]
164
+ * @returns {Promise<any>}
165
+ */
166
+ command(name: string, args?: Record<string, unknown>): Promise<any>;
167
+ /**
168
+ * Listens to an event (`pilot_pass`, …), to `'*'` (every server event) or
169
+ * to `hello`, `lagged`, `disconnected`. The listener gets `data` and the
170
+ * whole message. Returns a function that removes the listener.
171
+ * @template {string} K
172
+ * @param {K} name
173
+ * @param {Listener<K>} fn
174
+ * @returns {() => void}
175
+ */
176
+ on<K extends string>(name: K, fn: Listener<K>): () => void;
177
+ /** @template {string} K @param {K} name @param {Listener<K>} fn */
178
+ off<K extends string>(name: K, fn: Listener<K>): void;
179
+ /**
180
+ * Waits for the next event `name` for which `filter` is true (or for any,
181
+ * without a filter); resolves with its data. Rejects after `timeout_ms`.
182
+ * Needs a connected WebSocket (`connect()`).
183
+ * @template {string} K
184
+ * @param {K} name
185
+ * @param {{filter?: (data: K extends keyof ClientEvents ? ClientEvents[K] : any) => boolean, timeout_ms?: number}} [options]
186
+ * @returns {Promise<K extends keyof ClientEvents ? ClientEvents[K] : any>}
187
+ */
188
+ waitFor<K extends string>(name: K, { filter, timeout_ms }?: {
189
+ filter?: (data: K extends keyof ClientEvents ? ClientEvents[K] : any) => boolean;
190
+ timeout_ms?: number;
191
+ }): Promise<K extends keyof ClientEvents ? ClientEvents[K] : any>;
192
+ /**
193
+ * Opens the WebSocket; resolves with the `hello` (contains `state`).
194
+ * Once connected, a dropped connection is opened again by itself (waiting
195
+ * 0.5 s, 1 s, 2 s, … up to `max_backoff_ms`) until `close()`. If the very
196
+ * first attempt fails (wrong address or key), the promise rejects and
197
+ * nothing is retried.
198
+ * @returns {Promise<Hello>}
199
+ */
200
+ connect(): Promise<Hello>;
201
+ /** Closes the WebSocket for good (no reconnect). Commands go over HTTP again. */
202
+ close(): void;
203
+ #private;
204
+ }
205
+ /**
206
+ * What `on` can listen to: the server's events, `'*'` (every event) and
207
+ * three of the client's own: `hello` (connected, with the state),
208
+ * `lagged` (events were lost: fetch the state again) and `disconnected`.
209
+ */
210
+ export type ClientEvents = EventMap & {
211
+ "*": any;
212
+ hello: Hello;
213
+ lagged: {
214
+ missed: number;
215
+ };
216
+ disconnected: {
217
+ code: number;
218
+ reason: string;
219
+ };
220
+ };
221
+ export type Listener<K extends string> = (data: K extends keyof ClientEvents ? ClientEvents[K] : any, msg: EventMessage<any> | any) => void;
222
+ export type LaptimerOptions = {
223
+ /**
224
+ * e.g. "http://pi:5800"
225
+ */
226
+ url: string;
227
+ /**
228
+ * API key "lt_…"
229
+ */
230
+ key: string;
231
+ /**
232
+ * auto (default): commands over the WebSocket while it is connected, else HTTP
233
+ */
234
+ transport?: "auto" | "http" | undefined;
235
+ /**
236
+ * per command, default 15000
237
+ */
238
+ timeout_ms?: number | undefined;
239
+ /**
240
+ * longest wait between reconnects, default 10000
241
+ */
242
+ max_backoff_ms?: number | undefined;
243
+ };
244
+ import type { Channel } from './types.js';
245
+ import type { ChannelChange } from './types.js';
246
+ import type { Pilot } from './types.js';
247
+ import type { StartOptions } from './types.js';
248
+ import type { Session } from './types.js';
249
+ import type { State } from './types.js';
250
+ import type { Stats } from './types.js';
251
+ import type { Hello } from './types.js';
252
+ import type { EventMap } from './types.js';
253
+ import type { EventMessage } from './types.js';