@columenlabs/lumen-ui 0.1.127 → 0.1.129

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.
@@ -5,5 +5,18 @@ export declare class ApiError extends Error {
5
5
  readonly code?: string;
6
6
  constructor(message: string, method: string, url: string, status: number, code?: string);
7
7
  }
8
- /** A JSON request; `where` names the caller in the error log (`assistant.undo`, `workspace.read`). */
9
- export declare function apiJson<T>(url: string, init?: RequestInit, where?: string): Promise<T>;
8
+ /**
9
+ * How long a request waits for the server before it fails as unreachable, so a page never waits
10
+ * forever: reads give up sooner than writes, which may be doing slow work the person asked for.
11
+ */
12
+ export declare const API_TIMEOUT_MS: {
13
+ read: number;
14
+ write: number;
15
+ };
16
+ /**
17
+ * A JSON request; `where` names the caller in the error log (`assistant.undo`, `workspace.read`).
18
+ * It gives up after `API_TIMEOUT_MS` (or `timeoutMs`); a caller's own `signal` still aborts it.
19
+ */
20
+ export declare function apiJson<T>(url: string, init?: RequestInit & {
21
+ timeoutMs?: number;
22
+ }, where?: string): Promise<T>;
package/dist/api-fetch.js CHANGED
@@ -22,13 +22,24 @@ export class ApiError extends Error {
22
22
  this.code = code;
23
23
  }
24
24
  }
25
- /** A JSON request; `where` names the caller in the error log (`assistant.undo`, `workspace.read`). */
25
+ /**
26
+ * How long a request waits for the server before it fails as unreachable, so a page never waits
27
+ * forever: reads give up sooner than writes, which may be doing slow work the person asked for.
28
+ */
29
+ export const API_TIMEOUT_MS = { read: 15_000, write: 60_000 };
30
+ /**
31
+ * A JSON request; `where` names the caller in the error log (`assistant.undo`, `workspace.read`).
32
+ * It gives up after `API_TIMEOUT_MS` (or `timeoutMs`); a caller's own `signal` still aborts it.
33
+ */
26
34
  export async function apiJson(url, init = {}, where = 'api') {
27
35
  const method = (init.method ?? 'GET').toUpperCase();
36
+ const { timeoutMs, ...rest } = init;
37
+ const timeout = AbortSignal.timeout(timeoutMs ?? (method === 'GET' ? API_TIMEOUT_MS.read : API_TIMEOUT_MS.write));
28
38
  let res;
29
39
  try {
30
40
  res = await fetch(url, {
31
- ...init,
41
+ ...rest,
42
+ signal: init.signal ? AbortSignal.any([init.signal, timeout]) : timeout,
32
43
  headers: {
33
44
  accept: 'application/json',
34
45
  ...(init.body ? { 'content-type': 'application/json' } : {}),
@@ -37,9 +48,12 @@ export async function apiJson(url, init = {}, where = 'api') {
37
48
  });
38
49
  }
39
50
  catch (err) {
40
- if (err.name === 'AbortError')
51
+ // The caller's own abort is theirs to handle; running out of time is a failure like no network.
52
+ if (err.name === 'AbortError' && !timeout.aborted)
41
53
  throw err;
42
- const e = new ApiError(`Couldn't reach the server (${method} ${url})`, method, url, 0);
54
+ const e = new ApiError(timeout.aborted
55
+ ? `The server didn't answer in time (${method} ${url})`
56
+ : `Couldn't reach the server (${method} ${url})`, method, url, 0);
43
57
  reportError(e, 'shown', where);
44
58
  reportFailedFetch(apiRoute(url), 0);
45
59
  throw e;
package/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ export { type TasksConfig, TasksButton, TasksProvider, useTaskFor, useTasks } fr
75
75
  export { type McpServer, McpSettings } from './mcp-settings.js';
76
76
  export { lumenToast } from './lumen-toast.js';
77
77
  export { LumenShell, type LumenShellProps, type ShellUser, StatusItem, firstName, initials, maskAmount, useAmountsHidden, useApp, useShell, } from './shell.js';
78
- export { apiJson } from './api-fetch.js';
78
+ export { API_TIMEOUT_MS, apiJson } from './api-fetch.js';
79
79
  export { savedAnswers, type SavedAnswersOptions, type SavedAnswersPersister, } from './saved-answers.js';
80
80
  export { cn } from './lib/utils.js';
81
81
  export { type HealthProbe, StatusHealth } from './status-health.js';
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ export { TasksButton, TasksProvider, useTaskFor, useTasks } from './tasks.js';
82
82
  export { McpSettings } from './mcp-settings.js';
83
83
  export { lumenToast } from './lumen-toast.js';
84
84
  export { LumenShell, StatusItem, firstName, initials, maskAmount, useAmountsHidden, useApp, useShell, } from './shell.js';
85
- export { apiJson } from './api-fetch.js';
85
+ export { API_TIMEOUT_MS, apiJson } from './api-fetch.js';
86
86
  export { savedAnswers, } from './saved-answers.js';
87
87
  export { cn } from './lib/utils.js';
88
88
  export { StatusHealth } from './status-health.js';
@@ -168,7 +168,7 @@ function PeopleList({ frame, fields, onInvite, }) {
168
168
  const active = people.filter((p) => p.status === 'active').length;
169
169
  const disabled = people.filter((p) => p.status === 'disabled').length;
170
170
  const onlyMe = people.length <= 1;
171
- return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "at least 1 always kept" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: view.sync.ok ? 'Synced' : 'Out of step', caption: view.sync.at ? when(view.sync.at) : view.sync.detail, valueClassName: view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: onlyMe ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
171
+ return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "Can\u2019t remove the last admin" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: view.sync.ok ? 'Synced' : 'Out of step', caption: view.sync.at ? when(view.sync.at) : view.sync.detail, valueClassName: view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: onlyMe ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
172
172
  }
173
173
  /** One change over the ticked people. Apply clears the selection; Undo puts the change back. */
174
174
  function BulkBar({ frame, ids, onClear, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.127",
3
+ "version": "0.1.129",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.127",
52
+ "@columenlabs/lumen-ui-types": "0.1.129",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",