@volter/supercode-teams 0.3.142 → 0.3.143
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 +4 -0
- package/bin/workspace.mjs +6 -0
- package/package.json +2 -2
- package/server/event-routes.mjs +1 -1
- package/server/harness-routes.mjs +5 -0
- package/server/work-meter.mjs +84 -0
- package/worker/index.mjs +43 -16
package/README.md
CHANGED
|
@@ -188,3 +188,7 @@ Only the session custodian can grant this binding, in person or through an app t
|
|
|
188
188
|
no session-backed channel is created. The agent DM uses the exact resource `${ROOM}:dm:${PRINCIPAL}`.
|
|
189
189
|
`unbind` accepts the same flags. `--conversation` remains available for existing grants; it is mutually exclusive
|
|
190
190
|
with `--agent`. Opening a DM reads the existing session; sending a turn is a separate action.
|
|
191
|
+
|
|
192
|
+
## Read server work diagnostics
|
|
193
|
+
|
|
194
|
+
An administrator can run `supercode teams storage work --json` to read recent handler records and retained slow work. Records name normalized HTTP routes, catalog refreshes, transitions and maintenance steps, with SQL call and cursor scan/write counts. The Worker retains up to 32 slow records across a restart. Elapsed milliseconds are not CPU time; hosted per-query timing is unavailable. [Recorded private Worker proof](../../docs/evidence/teams-handler-work-2026-10-07.md).
|
package/bin/workspace.mjs
CHANGED
|
@@ -95,6 +95,11 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
95
95
|
if (verb === 'sessions') return sessionsCommand(sub, command);
|
|
96
96
|
if (verb === 'calendar') return calendarCommand(sub, { ...command, teamPath });
|
|
97
97
|
if (verb === 'settings') return settingsCommand(sub, command);
|
|
98
|
+
if (verb === 'storage') {
|
|
99
|
+
only(flags, ['json', 'help', 'context']);requireArgs(rest, 0, 0, 'storage work');
|
|
100
|
+
if(sub!=='work')usage('storage work');
|
|
101
|
+
return output(await client.request('GET',teamPath(context,'/storage/work')));
|
|
102
|
+
}
|
|
98
103
|
if (verb === 'members') return membersCommand(sub, command);
|
|
99
104
|
if (verb === 'groups') return groupsCommand(sub, command);
|
|
100
105
|
if (verb === 'sources') return sourcesCommand(sub, command);
|
|
@@ -1434,6 +1439,7 @@ function printHelp() {
|
|
|
1434
1439
|
identity link
|
|
1435
1440
|
identities observations|sightings|resolve|observe|declare-person|link|infer|revoke|revoke-inference --body-file FILE
|
|
1436
1441
|
server start [--data-dir DIR] [--listen HOST:PORT] [--public-origin ORIGIN] [--init-team NAME] | server token
|
|
1442
|
+
storage work [--context NAME] [--json] administrators: recent handler work and retained slow records
|
|
1437
1443
|
publish pane|root TARGET --room ROOM --workplace ORIGIN --organization ORG [--scope view|control] [--name] [--key] [--machine] [--token-stdin] [--record-only]
|
|
1438
1444
|
unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG [--machine] [--token-stdin] [--record-only|--workplace-only]
|
|
1439
1445
|
connect|disconnect|status|fold (fold: the connector's state back into connector.json for an older daemon, with it stopped)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.143",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@volter/supercode-health": "^0.1.0",
|
|
71
71
|
"@volter/supercode-terminal": "^0.2.54",
|
|
72
72
|
"@volter/supercode-trace": "^0.1.0",
|
|
73
|
-
"@volter/teams": "0.2.
|
|
73
|
+
"@volter/teams": "0.2.38",
|
|
74
74
|
"@xterm/addon-serialize": "^0.14.0",
|
|
75
75
|
"@xterm/headless": "^6.0.0",
|
|
76
76
|
"protobufjs": "8.8.0",
|
package/server/event-routes.mjs
CHANGED
|
@@ -221,7 +221,7 @@ export async function handleCatalogEvents({ api, req, res, auth, url, requestId,
|
|
|
221
221
|
const stream = await openStream(api, req, res, auth, requestId);
|
|
222
222
|
let lastCursor = null;
|
|
223
223
|
const enqueue = streamQueue(stream, async () => {
|
|
224
|
-
const page = await catalogWindow(api, auth, query);
|
|
224
|
+
const page = await (api.work ? api.work('catalogWindow', () => catalogWindow(api, auth, query)) : catalogWindow(api, auth, query));
|
|
225
225
|
// Authorization and expiry can change a window without creating a session revision.
|
|
226
226
|
// A binding's observed_at moves with every heartbeat; only a change of what the window says is a reset.
|
|
227
227
|
const digest = createHash('sha256').update(JSON.stringify(page.data, (key, value) => key === 'bindings' && Array.isArray(value) ? value.map(({ observed_at: _seen, ...binding }) => binding) : value)).digest('hex');
|
|
@@ -88,6 +88,11 @@ async function beforeIdempotency({ api, req, res, auth, url, path, sub, requestI
|
|
|
88
88
|
|
|
89
89
|
async function team({ api, req, res, auth, url, path, sub, requestId }) {
|
|
90
90
|
let m;
|
|
91
|
+
if(req.method==='GET'&&sub==='/storage/work'){
|
|
92
|
+
if(!isAdmin(auth))fail(403,'forbidden','Teams work diagnostics require an administrator');
|
|
93
|
+
if(!api.workReport)fail(501,'unsupported','this host does not record Teams work diagnostics');
|
|
94
|
+
send(res,200,result(api,auth,api.workReport()),requestId);return true;
|
|
95
|
+
}
|
|
91
96
|
if(req.method==='GET'&&sub==='/storage/uploads'){await storageReport({api,res,auth,url,requestId});return true;}
|
|
92
97
|
if(req.method==='GET'&&sub==='/sessions'){await listSessions(api,res,auth,url,requestId,false);return true;}
|
|
93
98
|
if(req.method==='GET'&&sub==='/sessions/search'){await listSessions(api,res,auth,url,requestId,true);return true;}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// Only contract path words survive. Never record a caller's query, identity, body or SQL parameters.
|
|
5
|
+
const WORDS = new Set('api v1 me health teams sessions messages search events catalogue machines heartbeat configuration roots sources captures parts commit uploads chunks members groups credentials permissions bindings agents publications attachments operations continuations sync-rules view-bindings exports audit storage work'.split(' '));
|
|
6
|
+
export function workRoute(input) {
|
|
7
|
+
try { return new URL(input, 'http://local').pathname.split('/').map(part => !part || WORDS.has(part) ? part : ':id').join('/'); }
|
|
8
|
+
catch { return '/:invalid'; }
|
|
9
|
+
}
|
|
10
|
+
const rounded = value => Math.round(value * 1000) / 1000;
|
|
11
|
+
|
|
12
|
+
/** Attribute synchronous SQLite work to its handler, rather than the invocation a timer happened to inherit. */
|
|
13
|
+
export class WorkMeter {
|
|
14
|
+
constructor({ emit = record => console.log(JSON.stringify(record)), now = () => performance.now(), retainSlow = null, advanceClock = null, sqlTiming = true } = {}) {
|
|
15
|
+
this.now = now; this.advanceClock = advanceClock; this.sqlTiming = sqlTiming;
|
|
16
|
+
this.context = new AsyncLocalStorage(); this.recent = []; this.slow = [];
|
|
17
|
+
this.emit = record => {
|
|
18
|
+
emit(record);
|
|
19
|
+
if (!['teams_work', 'teams_sql_work'].includes(record.event)) return;
|
|
20
|
+
this.recent.push(record); if (this.recent.length > 64) this.recent.shift();
|
|
21
|
+
if ((record.elapsed_ms ?? 0) < 1000 && (record.rows_read ?? 0) + (record.rows_written ?? 0) < 5000) return;
|
|
22
|
+
this.slow.push(record); if (this.slow.length > 32) this.slow.shift();
|
|
23
|
+
try { retainSlow?.(this.slow); }
|
|
24
|
+
catch { emit({ event: 'teams_work_retention_failed', work_id: record.work_id ?? null }); }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
restoreSlow(records) { if (Array.isArray(records)) this.slow = records.slice(-32); }
|
|
28
|
+
report() { return { clock: this.advanceClock ? 'elapsed milliseconds through an end-of-work timer boundary; SQL timing unavailable on the hosted Worker' : 'elapsed milliseconds; sql_ms measures synchronous SQL, not CPU', recent: this.recent.slice(), slow: this.slow.slice() }; }
|
|
29
|
+
async run(work, fn, detail = {}) {
|
|
30
|
+
const parent = this.context.getStore();
|
|
31
|
+
const record = { work, ...detail, work_id: crypto.randomUUID(), parent_work_id: parent?.work_id ?? null, started_at: new Date().toISOString(), started: this.now(), sql_ms: 0, sql_calls: 0, rows_read: 0, rows_written: 0, rows_known: true, returned_rows: 0, slowest_sql: null, most_rows_sql: null, finished: false };
|
|
32
|
+
this.emit({ event: 'teams_work_start', work, ...detail, work_id: record.work_id, parent_work_id: record.parent_work_id, started_at: record.started_at });
|
|
33
|
+
return this.context.run(record, async () => {
|
|
34
|
+
let outcome = 'ok';
|
|
35
|
+
try { return await fn(); }
|
|
36
|
+
catch (error) { outcome = 'error'; throw error; }
|
|
37
|
+
finally {
|
|
38
|
+
record.finished = true;
|
|
39
|
+
// Hosted Workers freeze clocks during CPU work. Yield once after the work, not once per SQL call.
|
|
40
|
+
// This reports elapsed time, including the timer's wait; it does not manufacture a CPU reading.
|
|
41
|
+
if (this.advanceClock) await this.advanceClock();
|
|
42
|
+
const { started, finished: _finished, rows_known, ...data } = record;
|
|
43
|
+
this.emit({ event: 'teams_work', ...data, slowest_sql: this.sqlTiming ? record.slowest_sql : null, rows_read: rows_known ? record.rows_read : null, rows_written: rows_known ? record.rows_written : null, elapsed_ms: rounded(this.now() - started), sql_ms: this.sqlTiming ? rounded(record.sql_ms) : null, outcome });
|
|
44
|
+
// Nested steps are separately named, and their SQL remains counted in the enclosing handler.
|
|
45
|
+
if (parent && !parent.finished) {
|
|
46
|
+
for (const field of ['sql_ms', 'sql_calls', 'rows_read', 'rows_written', 'returned_rows']) parent[field] += record[field];
|
|
47
|
+
parent.rows_known &&= record.rows_known;
|
|
48
|
+
if (record.slowest_sql && (!parent.slowest_sql || record.slowest_sql.elapsed_ms > parent.slowest_sql.elapsed_ms)) parent.slowest_sql = record.slowest_sql;
|
|
49
|
+
if (record.most_rows_sql && (!parent.most_rows_sql || record.most_rows_sql.rows_read + record.most_rows_sql.rows_written > parent.most_rows_sql.rows_read + parent.most_rows_sql.rows_written)) parent.most_rows_sql = record.most_rows_sql;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
detail(values) {
|
|
55
|
+
const record = this.context.getStore();
|
|
56
|
+
if (record && !record.finished) Object.assign(record, values);
|
|
57
|
+
}
|
|
58
|
+
sql(query, fn) {
|
|
59
|
+
const record = this.context.getStore();
|
|
60
|
+
if (!record || record.finished) {
|
|
61
|
+
const start = this.now(); let result;
|
|
62
|
+
try { result = fn(); return result; }
|
|
63
|
+
finally {
|
|
64
|
+
const elapsed = this.now() - start;
|
|
65
|
+
if (elapsed >= 100 || (result?.rowsRead ?? 0) >= 5000) this.emit({ event: 'teams_sql_work', work: 'unscoped', origin_work: record?.work ?? null, fingerprint: createHash('sha256').update(query).digest('hex').slice(0, 16), elapsed_ms: this.sqlTiming ? rounded(elapsed) : null, rows_read: result?.rowsRead ?? null, rows_written: result?.rowsWritten ?? null, returned_rows: result?.returnedRows ?? null });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const start = this.now(); let result;
|
|
69
|
+
try { result = fn(); return result; }
|
|
70
|
+
finally {
|
|
71
|
+
const elapsed = this.now() - start;
|
|
72
|
+
record.sql_calls++; record.sql_ms += elapsed;
|
|
73
|
+
record.rows_known &&= Number.isFinite(result?.rowsRead) && Number.isFinite(result?.rowsWritten);
|
|
74
|
+
record.rows_read += result?.rowsRead ?? 0; record.rows_written += result?.rowsWritten ?? 0;
|
|
75
|
+
record.returned_rows += result?.returnedRows ?? 0;
|
|
76
|
+
if (!record.slowest_sql || elapsed > record.slowest_sql.elapsed_ms) {
|
|
77
|
+
record.slowest_sql = { fingerprint: createHash('sha256').update(query).digest('hex').slice(0, 16), operation: /^\s*(\w+)/.exec(query)?.[1]?.toUpperCase() ?? 'UNKNOWN', elapsed_ms: rounded(elapsed), rows_read: result?.rowsRead ?? null, rows_written: result?.rowsWritten ?? null, returned_rows: result?.returnedRows ?? null };
|
|
78
|
+
}
|
|
79
|
+
if (Number.isFinite(result?.rowsRead) && Number.isFinite(result?.rowsWritten) && (!record.most_rows_sql || result.rowsRead + result.rowsWritten > record.most_rows_sql.rows_read + record.most_rows_sql.rows_written)) {
|
|
80
|
+
record.most_rows_sql = { fingerprint: createHash('sha256').update(query).digest('hex').slice(0, 16), operation: /^\s*(\w+)/.exec(query)?.[1]?.toUpperCase() ?? 'UNKNOWN', rows_read: result.rowsRead, rows_written: result.rowsWritten, returned_rows: result?.returnedRows ?? null };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/worker/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { setOtlpSchemaSources } from '../server/otlp/transport.mjs';
|
|
|
18
18
|
import { setViewerSources } from '@volter/teams/server/viewer';
|
|
19
19
|
import { serveFetch } from './fetch-adapter.mjs';
|
|
20
20
|
import { webSocketDuplex } from './websocket-duplex.mjs';
|
|
21
|
+
import { WorkMeter, workRoute } from '../server/work-meter.mjs';
|
|
21
22
|
// The hosted viewer's files, bundled as text (wrangler.toml rules) and served at /view.
|
|
22
23
|
import viewerPage from '../../volter-teams/viewer/index.html';
|
|
23
24
|
import viewerScript from '../../volter-teams/viewer/viewer.mjs';
|
|
@@ -56,31 +57,47 @@ const MAINTAIN_MS = 60_000;
|
|
|
56
57
|
const BOOTSTRAP_TOKEN = /^sct_(cred_[0-9a-f]{32})\.[A-Za-z0-9_-]{16,}$/;
|
|
57
58
|
|
|
58
59
|
/** SQLite primitives over the object's storage, in store-core's shape. */
|
|
59
|
-
function durableSql(storage) {
|
|
60
|
+
function durableSql(storage, meter) {
|
|
60
61
|
const sql = storage.sql;
|
|
61
62
|
const param = (value) => (value instanceof Uint8Array ? value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) : value);
|
|
62
63
|
const row = (value) => { for (const key of Object.keys(value)) if (value[key] instanceof ArrayBuffer) value[key] = Buffer.from(value[key]); return value; };
|
|
63
|
-
const
|
|
64
|
+
const measured = (query, fn) => meter.sql(query, fn);
|
|
65
|
+
const all = (query, params = []) => measured(query, () => {
|
|
66
|
+
const cursor = sql.exec(query, ...params.map(param));
|
|
67
|
+
const values = cursor.toArray().map(row);
|
|
68
|
+
return { values, rowsRead: cursor.rowsRead, rowsWritten: cursor.rowsWritten, returnedRows: values.length };
|
|
69
|
+
}).values;
|
|
64
70
|
const get = (query, params = []) => all(query, params)[0] ?? null;
|
|
65
|
-
const run = (query, params = []) => {
|
|
71
|
+
const run = (query, params = []) => {
|
|
72
|
+
const counts = measured(query, () => {
|
|
73
|
+
const cursor = sql.exec(query, ...params.map(param));
|
|
74
|
+
return { rowsRead: cursor.rowsRead, rowsWritten: cursor.rowsWritten, returnedRows: 0 };
|
|
75
|
+
});
|
|
76
|
+
return { ...counts, changes: get('SELECT changes() AS c').c };
|
|
77
|
+
};
|
|
66
78
|
const tx = (fn) => storage.transactionSync(fn);
|
|
67
|
-
return { all, get, run, tx, exec: (query) =>
|
|
79
|
+
return { all, get, run, tx, exec: (query) => { all(query); } };
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
export class TeamsServer extends DurableObject {
|
|
71
83
|
constructor(ctx, env) {
|
|
72
84
|
super(ctx, env);
|
|
73
|
-
this.
|
|
85
|
+
this.work = new WorkMeter({ retainSlow: records => ctx.storage.kv.put('teams_work_slow_v1', JSON.stringify(records)), advanceClock: () => scheduler.wait(0), sqlTiming: false });
|
|
86
|
+
this.ready = ctx.blockConcurrencyWhile(() => this.work.run('activation', async () => {
|
|
74
87
|
// Read before migrations/maintenance: allocation failures must not hide
|
|
75
88
|
// the table responsible, including hosts with a lower storage limit.
|
|
76
89
|
console.log(`storage: database_bytes=${ctx.storage.sql.databaseSize}`);
|
|
77
|
-
const db = durableSql(ctx.storage);
|
|
90
|
+
const db = durableSql(ctx.storage, this.work);
|
|
78
91
|
migrate({ exec: db.exec, tx: db.tx, version: () => ctx.storage.kv.get('schema_version') ?? 0, setVersion: (n) => ctx.storage.kv.put('schema_version', n) });
|
|
92
|
+
const retained = ctx.storage.kv.get('teams_work_slow_v1');
|
|
93
|
+
if (retained) this.work.restoreSlow(JSON.parse(retained));
|
|
79
94
|
this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), appRootSecret: env.SUPERCODE_TEAMS_BOOTSTRAP_TOKEN || null });
|
|
95
|
+
this.api.work = (name, fn, detail) => this.work.run(name, fn, detail);
|
|
96
|
+
this.api.workReport = () => this.work.report();
|
|
80
97
|
this.api.storageUsage = () => {
|
|
81
98
|
const sql = ctx.storage.sql;
|
|
82
99
|
let freeBytes = null;
|
|
83
|
-
try { freeBytes =
|
|
100
|
+
try { freeBytes = db.get('PRAGMA freelist_count').freelist_count * db.get('PRAGMA page_size').page_size; } catch { /* preserve an explicit unknown if the host omits these pragmas */ }
|
|
84
101
|
return { allocated: sql.databaseSize, free_bytes: freeBytes, used: Math.max(0, sql.databaseSize - (freeBytes ?? 0)), used_is_upper_bound: freeBytes === null, limit: 10 * 1024 ** 3 };
|
|
85
102
|
};
|
|
86
103
|
this.api.attach({ store: new DirectStore(createStoreHandlers(db)), harnessRoutes });
|
|
@@ -93,7 +110,16 @@ export class TeamsServer extends DurableObject {
|
|
|
93
110
|
this.api.attach({ serverId: await bootstrapServer(this.api, { initTeam: env.INIT_TEAM, bootstrap: { server_id: ids.next('server'), team_id: ids.next('team'), principal_id: ids.next('user'), credential_id: match[1], token: env.SUPERCODE_TEAMS_BOOTSTRAP_TOKEN } }) });
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
|
-
|
|
113
|
+
const handler = createHttpHandler(this.api);
|
|
114
|
+
this.handler = (req, res) => {
|
|
115
|
+
const pending = this.work.run('http', async () => {
|
|
116
|
+
const writeHead = res.writeHead;
|
|
117
|
+
res.writeHead = (...args) => { this.work.detail({ status: args[0], request_id: args[1]?.['X-Request-ID'] ?? null }); return writeHead.apply(res, args); };
|
|
118
|
+
return handler(req, res);
|
|
119
|
+
}, { method: req.method, route: workRoute(req.url) });
|
|
120
|
+
ctx.waitUntil(pending);
|
|
121
|
+
return pending;
|
|
122
|
+
};
|
|
97
123
|
// Retention never runs here: an activation serves its first request as soon as the store is open, and a reset is
|
|
98
124
|
// never followed by a pass that holds every request back (t_6c01c966). A capacity failure can exhaust an old
|
|
99
125
|
// alarm's retries, and an exhausted alarm still reads its old time, already past, while it never fires again: so
|
|
@@ -101,13 +127,13 @@ export class TeamsServer extends DurableObject {
|
|
|
101
127
|
const armed = await ctx.storage.getAlarm().catch(() => null);
|
|
102
128
|
const now = Date.now(), soon = now + 5_000;
|
|
103
129
|
if (armed == null || armed > soon || armed < now) await ctx.storage.setAlarm(soon).catch(error => console.error(`maintain alarm scheduling failed: ${error.message}`));
|
|
104
|
-
});
|
|
130
|
+
}));
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
async fetch(request) {
|
|
108
134
|
await this.ready;
|
|
109
135
|
if (!this.api.serverId) return Response.json({ version: 1, error: { code: 'unavailable', message: 'set the SUPERCODE_TEAMS_BOOTSTRAP_TOKEN secret (supercode teams server token) before first use' } }, { status: 503 });
|
|
110
|
-
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') return this.upgrade(request);
|
|
136
|
+
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') return this.work.run('websocket.upgrade', () => this.upgrade(request), { method: request.method, route: workRoute(request.url) });
|
|
111
137
|
return serveFetch(this.handler, request, { remoteAddress: request.headers.get('cf-connecting-ip') });
|
|
112
138
|
}
|
|
113
139
|
|
|
@@ -128,7 +154,7 @@ export class TeamsServer extends DurableObject {
|
|
|
128
154
|
|
|
129
155
|
async alarm() {
|
|
130
156
|
await this.ready;
|
|
131
|
-
const done = this.api.serverId ? await maintain(this.api).catch(error => { console.error(`maintain failed: ${error.message}`); return null; }) : null;
|
|
157
|
+
const done = this.api.serverId ? await this.work.run('alarm', () => maintain(this.api)).catch(error => { console.error(`maintain failed: ${error.message}`); return null; }) : null;
|
|
132
158
|
const usage = this.api.storageUsage();
|
|
133
159
|
if (usage.used >= usage.limit * 0.8) console.warn(`Teams storage pressure: ${JSON.stringify(usage)}; native upload admission stops at 90%`);
|
|
134
160
|
// The database's size on every full pass, where `wrangler tail` reads it.
|
|
@@ -156,6 +182,7 @@ const objectFailure = (error) => ({
|
|
|
156
182
|
export default {
|
|
157
183
|
async fetch(request, env) {
|
|
158
184
|
const repeatable = request.method === 'GET' || request.method === 'HEAD';
|
|
185
|
+
const route = { method: request.method, route: workRoute(request.url) };
|
|
159
186
|
const call = () => env.TEAMS_SERVER.get(env.TEAMS_SERVER.idFromName('server')).fetch(repeatable ? request.clone() : request);
|
|
160
187
|
// A request safe to repeat (a read, or a link's WebSocket upgrade: every machine's connection) is tried once more
|
|
161
188
|
// when the runtime says the failure is retryable and the object is not overloaded: a reset object (its code
|
|
@@ -166,10 +193,10 @@ export default {
|
|
|
166
193
|
try { return await call(); }
|
|
167
194
|
catch (error) {
|
|
168
195
|
first = objectFailure(error);
|
|
169
|
-
if (!(repeatable && first.retryable && !first.overloaded)) return unavailable(first, null);
|
|
196
|
+
if (!(repeatable && first.retryable && !first.overloaded)) return unavailable(first, null, route);
|
|
170
197
|
}
|
|
171
198
|
try { return await call(); }
|
|
172
|
-
catch (error) { return unavailable(objectFailure(error), first); }
|
|
199
|
+
catch (error) { return unavailable(objectFailure(error), first, route); }
|
|
173
200
|
},
|
|
174
201
|
};
|
|
175
202
|
|
|
@@ -180,9 +207,9 @@ const PUBLIC_NAMES = new Set(['Error', 'TypeError', 'RangeError', 'ReferenceErro
|
|
|
180
207
|
/** Failures before the HTTP handler (object initialization, migrations, storage, a reset) as our API contract, not a
|
|
181
208
|
* platform HTML page. The answer goes to callers not yet authenticated, so it names only the error's kind and the
|
|
182
209
|
* runtime's flags; its message (storage or SQL text, internal paths) is in the log line its request id leads to. */
|
|
183
|
-
function unavailable(failure, retried) {
|
|
210
|
+
function unavailable(failure, retried, route) {
|
|
184
211
|
const requestId = crypto.randomUUID();
|
|
185
|
-
console.error(JSON.stringify({ event: 'teams_unavailable', request_id: requestId, failure, ...(retried ? { first: retried } : {}) }));
|
|
212
|
+
console.error(JSON.stringify({ event: 'teams_unavailable', request_id: requestId, ...route, failure, ...(retried ? { first: retried } : {}) }));
|
|
186
213
|
const cause = `${PUBLIC_NAMES.has(failure.name) ? failure.name : 'Error'}${failure.reset ? ', object reset' : ''}${failure.overloaded ? ', overloaded' : ''}${retried ? ', after one retry' : ''}`;
|
|
187
|
-
return Response.json({ version: 1, error: { code: 'unavailable', message: `Teams server is temporarily unavailable (${cause})`, request_id: requestId, retryable: true, cause: { name: PUBLIC_NAMES.has(failure.name) ? failure.name : 'Error', retryable: failure.retryable, overloaded: failure.overloaded, remote: failure.remote, reset: failure.reset } } }, { status: 503, headers: { 'Retry-After': failure.overloaded ? '15' : '5', 'X-Request-ID': requestId } });
|
|
214
|
+
return Response.json({ version: 1, error: { code: 'unavailable', message: `Teams server is temporarily unavailable (${cause})`, request_id: requestId, ...route, retryable: true, cause: { name: PUBLIC_NAMES.has(failure.name) ? failure.name : 'Error', retryable: failure.retryable, overloaded: failure.overloaded, remote: failure.remote, reset: failure.reset } } }, { status: 503, headers: { 'Retry-After': failure.overloaded ? '15' : '5', 'X-Request-ID': requestId } });
|
|
188
215
|
}
|