@acms-subir/client 0.1.8 → 0.1.10

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
@@ -49,6 +49,7 @@ the cache and transport configuration with the original client.
49
49
  | `unregisterDevice(token)` | Opt out (idempotent) |
50
50
  | `pushTopics()` | Topics to offer as notification toggles |
51
51
  | `links()` | The links page: profile, socials and visible blocks (`link`, `email`, `phone`, `contact`, `heading`, `text`, `spacer`, `live`, `embed`) |
52
+ | `live()` | Live video: the current broadcast (`snapshot` or `null`), `serverNow` and the owner's `display` choices. Never cached; 404 while `site.features.live` is false |
52
53
  | `form(id)`, `submitForm(id, { answers, version?, idempotencyKey? })` | A published form's questions; send answers (checkbox groups as arrays). Each call makes one key; retries reuse it |
53
54
  | `manageBooking(token)`, `cancelBooking(token)` | The booker's own booking from its `manageToken`, and cancelling it; `can` says what is allowed |
54
55
  | `rescheduleAvailability(token, { from?, days?, minutes? })`, `rescheduleBooking(token, { start, minutes? })` | Open times for moving it (its own time counts as free), then the move |
@@ -140,12 +141,34 @@ await client.updateDevice(token, { topics: ['events'] });
140
141
  await client.unregisterDevice(token);
141
142
  ```
142
143
 
144
+ ## Live video
145
+
146
+ `site.features.live` is true while the site's live video (Integrations → Live
147
+ video) isn't off. `live()` returns the same snapshot the website shows:
148
+ `status` (`live`, `upcoming` or `offline` for the "Watch our channel" fallback),
149
+ `title`, `watchUrl`, an absolute `thumbUrl` (empty when there's none),
150
+ `startedAt`, `expiresAt` and optional `channel` and `viewers`. Times are Unix
151
+ milliseconds on the **server's** clock: keep `serverNow - Date.now()` as an
152
+ offset, and stop treating a snapshot as current once `expiresAt` (when nonzero)
153
+ passes on that clock, even if later requests fail. `display` carries the
154
+ owner's choices (`style`: `poster`, `split` or `row`; `bar`, `header`, `dock`)
155
+ so an app can follow the website. Poll about every 30 seconds while the app is
156
+ in the foreground; `@acms-subir/native`'s `useLive()` does all of this. The
157
+ "we're live" notification is the push topic `live`.
158
+
159
+ ```ts
160
+ const { snapshot, serverNow } = await client.live();
161
+ const offset = serverNow - Date.now();
162
+ const current = snapshot && (!snapshot.expiresAt || snapshot.expiresAt > Date.now() + offset) ? snapshot : null;
163
+ if (current?.status === 'live') console.log(`Live: ${current.title} → ${current.watchUrl}`);
164
+ ```
165
+
143
166
  ## Cache and headers
144
167
 
145
168
  ETag revalidation defaults to an in-memory URL-keyed cache. Each read contacts
146
169
  the server with `If-None-Match`; a 304 reuses the saved data. This is not an
147
170
  offline fallback. Treat returned cached objects as immutable. Use `cache: false`
148
- to disable it. Availability, booking results and errors are never cached.
171
+ to disable it. Availability, live status, booking results and errors are never cached.
149
172
  Responses without an ETag or with `no-store` invalidate an older entry.
150
173
 
151
174
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics } from './types.js';
1
+ import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus } from './types.js';
2
2
  export { AcmsError, isAcmsError } from './errors.js';
3
3
  export type * from './types.js';
4
4
  export * from './editor.js';
@@ -164,6 +164,10 @@ export declare const endpoints: {
164
164
  readonly method: "POST";
165
165
  readonly path: "/forms/{id}/responses";
166
166
  };
167
+ readonly live: {
168
+ readonly method: "GET";
169
+ readonly path: "/live";
170
+ };
167
171
  readonly manageBooking: {
168
172
  readonly method: "GET";
169
173
  readonly path: "/bookings/manage";
@@ -236,6 +240,12 @@ export interface AcmsClient {
236
240
  pushTopics(o?: RequestOptions): Promise<PushTopics>;
237
241
  /** The links page: profile, socials and visible blocks. Requires `site.features.links`. */
238
242
  links(o?: RequestOptions): Promise<Links>;
243
+ /**
244
+ * The current broadcast (`snapshot`, or null), the server's clock (`serverNow`, ms) and the owner's
245
+ * display choices. Never cached. A live snapshot stops counting once `expiresAt` passes on the server's
246
+ * clock, even if later requests fail. Requires `site.features.live`.
247
+ */
248
+ live(o?: RequestOptions): Promise<LiveStatus>;
239
249
  /** A published form's questions. 410 when it stopped accepting responses. Requires `site.features.forms`. */
240
250
  form(id: string, o?: RequestOptions): Promise<Form>;
241
251
  /** Send answers (checkbox groups as arrays). Retries reuse the same Idempotency-Key, so they never duplicate. */
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ export const endpoints = {
15
15
  unregisterDevice: { method: 'DELETE', path: '/devices/{token}' }, pushTopics: { method: 'GET', path: '/push/topics' },
16
16
  // links page and forms (owner switches in Admin → Mobile app)
17
17
  links: { method: 'GET', path: '/links' }, form: { method: 'GET', path: '/forms/{id}' }, submitForm: { method: 'POST', path: '/forms/{id}/responses' },
18
+ // live video status (404 while the site's live video is off)
19
+ live: { method: 'GET', path: '/live' },
18
20
  // the booker's own booking: its private token goes in the Booking-Token header
19
21
  manageBooking: { method: 'GET', path: '/bookings/manage' }, cancelBooking: { method: 'POST', path: '/bookings/manage/cancel' },
20
22
  rescheduleAvailability: { method: 'GET', path: '/bookings/manage/availability' }, rescheduleBooking: { method: 'POST', path: '/bookings/manage/reschedule' },
@@ -105,7 +107,8 @@ export function createClient(options) {
105
107
  opts.signal?.addEventListener('abort', abort, { once: true });
106
108
  const timer = setTimeout(() => controller.abort(new AcmsError(0, 'timeout', 'Request timed out.', undefined, undefined, key)), timeoutMs);
107
109
  const signal = controller.signal;
108
- const cacheable = endpoint.method === 'GET' && name !== 'availability' && !extra.headers && cache;
110
+ // availability and live status are never served from the cache
111
+ const cacheable = endpoint.method === 'GET' && name !== 'availability' && name !== 'live' && !extra.headers && cache;
109
112
  // only replay what the server treats as repeatable: a lost reply to a code
110
113
  // exchange or a move must not run it twice
111
114
  const replayable = endpoint.method !== 'POST' || !!key || name === 'registerDevice';
@@ -274,6 +277,7 @@ export function createClient(options) {
274
277
  unregisterDevice: (token, o = {}) => request('unregisterDevice', o, {}, token),
275
278
  pushTopics: (o = {}) => request('pushTopics', o),
276
279
  links: (o = {}) => request('links', o),
280
+ live: (o = {}) => request('live', o),
277
281
  form: (id, o = {}) => request('form', o, {}, id),
278
282
  submitForm: (id, input, o = {}) => {
279
283
  const { idempotencyKey, ...body } = input;
package/dist/types.d.ts CHANGED
@@ -25,6 +25,7 @@ export type Site = {
25
25
  "key": string;
26
26
  "name": string;
27
27
  "url": string;
28
+ "redirectFrom": Array<string>;
28
29
  }>;
29
30
  "calendar": boolean;
30
31
  "bookings": boolean;
@@ -33,6 +34,7 @@ export type Site = {
33
34
  "guestSignIn": boolean;
34
35
  "forms": boolean;
35
36
  "links": boolean;
37
+ "live": boolean;
36
38
  };
37
39
  "apiVersion": 1;
38
40
  };
@@ -49,6 +51,7 @@ export type Theme = {
49
51
  "accent": string;
50
52
  "panel": string;
51
53
  "soft": string;
54
+ "live": string;
52
55
  };
53
56
  "dark": {
54
57
  "bg": string;
@@ -58,6 +61,7 @@ export type Theme = {
58
61
  "accent": string;
59
62
  "panel": string;
60
63
  "soft": string;
64
+ "live": string;
61
65
  };
62
66
  };
63
67
  "fonts": {
@@ -804,6 +808,45 @@ export type PushTopics = {
804
808
  "kind": "general" | "collection" | "calendar";
805
809
  }>;
806
810
  };
811
+ export type LiveSnapshot = {
812
+ "status": "live" | "upcoming" | "offline" | "unknown";
813
+ "source": "automatic" | "manual";
814
+ "provider": "youtube" | "twitch" | "facebook" | "other";
815
+ "id": string;
816
+ "title": string;
817
+ "watchUrl": string;
818
+ "embedId": string;
819
+ "startedAt": number;
820
+ "checkedAt": number;
821
+ "expiresAt": number;
822
+ "channel"?: string;
823
+ "viewers"?: number;
824
+ "thumbUrl": string;
825
+ };
826
+ export type LiveStatus = {
827
+ "snapshot": ({
828
+ "status": "live" | "upcoming" | "offline" | "unknown";
829
+ "source": "automatic" | "manual";
830
+ "provider": "youtube" | "twitch" | "facebook" | "other";
831
+ "id": string;
832
+ "title": string;
833
+ "watchUrl": string;
834
+ "embedId": string;
835
+ "startedAt": number;
836
+ "checkedAt": number;
837
+ "expiresAt": number;
838
+ "channel"?: string;
839
+ "viewers"?: number;
840
+ "thumbUrl": string;
841
+ }) | (null);
842
+ "serverNow": number;
843
+ "display": {
844
+ "style": "poster" | "split" | "row";
845
+ "bar": boolean;
846
+ "header": boolean;
847
+ "dock": boolean;
848
+ };
849
+ };
807
850
  export type ApiErrorResponse = {
808
851
  "version": 1;
809
852
  "error": {
@@ -2312,3 +2355,4 @@ export type SearchEventHit = SearchResults["events"][number];
2312
2355
  export type LinkItem = Links["items"][number];
2313
2356
  export type FormField = Form["fields"][number];
2314
2357
  export type GuestBooking = GuestBookings["items"][number];
2358
+ export type LiveDisplay = LiveStatus["display"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/client",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Typed, dependency-free client for the ACMS public mobile API.",
5
5
  "type": "module",
6
6
  "license": "MIT",