@volter/teams 0.1.33 → 0.1.34

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/viewer/viewer.mjs +23 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
package/viewer/viewer.mjs CHANGED
@@ -17,6 +17,20 @@ let waiting = null;
17
17
 
18
18
  const embedded = window.parent !== window;
19
19
  const ask = (type) => { if (embedded) window.parent.postMessage({ type }, '*'); };
20
+ // Where a failure is reported: the session this page shows, once it knows it (the server logs it under the token's member).
21
+ let reportTo = null;
22
+ /**
23
+ * A failure this page cannot recover from by itself (a read, the follow, the page's own code): shown as a plain line,
24
+ * posted to the host as `viewer-error` { stage, status, message }, and sent to the server's log.
25
+ */
26
+ function failed(stage, error, statusCode = null) {
27
+ const message = String(error?.message ?? error ?? 'unknown error').slice(0, 400);
28
+ status(message);
29
+ if (embedded) window.parent.postMessage({ type: 'viewer-error', stage, status: statusCode, message }, '*');
30
+ if (reportTo && token) fetch(reportTo, { method: 'POST', keepalive: true, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ stage, status: statusCode, message }) }).catch(() => {});
31
+ }
32
+ window.addEventListener('error', (event) => failed('page', event.error ?? event.message));
33
+ window.addEventListener('unhandledrejection', (event) => failed('page', event.reason));
20
34
  // The host's style, as supercode-ui is themed: its `--volter-*` brand tokens and `--scui-*` component tokens, set on
21
35
  // the session's root (every component reads them from an ancestor). Nothing else is taken from the host.
22
36
  function applyTheme(properties) {
@@ -120,8 +134,9 @@ async function showSession(session, control) {
120
134
  stylesheet.rel = 'stylesheet';
121
135
  stylesheet.href = '/view/session-view.css';
122
136
  document.head.append(stylesheet);
123
- const { mountSessionView } = await import('/view/session-view.mjs');
124
137
  const base = `${location.origin}/api/v1/teams/${encodeURIComponent(session.team)}/sessions/${encodeURIComponent(session.id)}`;
138
+ reportTo = `${base}/viewer-errors`;
139
+ const { mountSessionView } = await import('/view/session-view.mjs').catch((error) => { throw Object.assign(error, { stage: 'load' }); });
125
140
  const element = $('session');
126
141
  element.hidden = false;
127
142
  const view = mountSessionView(element, {
@@ -159,7 +174,7 @@ async function showSession(session, control) {
159
174
  const read = () => (reading ??= (async () => {
160
175
  try {
161
176
  const response = await fetch(`${base}/messages?tail=300`, { headers: await headers() });
162
- if (!response.ok) throw new Error(`reading the session failed (${response.status})`);
177
+ if (!response.ok) throw Object.assign(new Error(`reading the session failed (${response.status})`), { stage: 'read', status: response.status });
163
178
  const body = await response.json();
164
179
  apply(body);
165
180
  return body;
@@ -173,7 +188,7 @@ async function showSession(session, control) {
173
188
  try {
174
189
  if (!cursor) cursor = String((await read()).event_cursor ?? '') || null;
175
190
  const response = await fetch(`${base}/events?${cursor ? `after=${encodeURIComponent(cursor)}` : 'tail=1'}`, { headers: await headers() });
176
- if (!response.ok) throw new Error(`following the session failed (${response.status})`);
191
+ if (!response.ok) throw Object.assign(new Error(`following the session failed (${response.status})`), { stage: 'follow', status: response.status });
177
192
  view.update({ live: true });
178
193
  status('');
179
194
  const reader = response.body.getReader();
@@ -193,12 +208,14 @@ async function showSession(session, control) {
193
208
  // The server ends a stream it can no longer resume (a slow reader, a retired cursor): start from a snapshot.
194
209
  if (event.terminal && event.type === 'resync_required') cursor = null;
195
210
  if (event.type === 'snapshot') apply(event.data);
196
- else if (event.type === 'session_changed') read().catch((error) => status(error.message));
211
+ else if (event.type === 'session_changed') read().catch((error) => failed('read', error, error.status ?? null));
197
212
  else if (event.type === 'session_deleted') { view.update({ live: false, status: 'error', error: 'This session was deleted.' }); return; }
198
213
  }
199
214
  }
200
215
  } catch (error) {
201
- status(error.message);
216
+ failed(error.stage ?? 'follow', error, error.status ?? null);
217
+ // Nothing drawn yet: the transcript says why instead of loading forever.
218
+ if (!cursor) view.update({ status: 'error', error: error.message });
202
219
  }
203
220
  view.update({ live: false });
204
221
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -320,4 +337,4 @@ async function showFolder(connect, root, control) {
320
337
  await list(base);
321
338
  }
322
339
 
323
- main().catch((error) => status(error.message));
340
+ main().catch((error) => failed(error?.stage ?? 'start', error, error?.status ?? null));