Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ Available commands:
- `--limit <n>` max results (default: 10)
- `session <code>` looks up a specific session by code. Searches all cached events; disambiguates if the code appears in multiple events.
- `--event <id>` scope to a specific event
- `refresh` downloads and caches session catalogs.
- `--event <id>` refresh a specific event only
- `--force` bypass cache, re-fetch unconditionally
- `refresh` checks for session catalog updates and updates the local cache.
- `--event <id>` check a specific event only
- `--force` bypass conditional revalidation and re-fetch unconditionally
- `status` shows what's cached and how fresh it is.

The `sessions` and `session` commands output human-readable text by default. Pass `--json` to get structured JSON, which is useful for piping to agents or other tools:
Expand All @@ -77,8 +77,9 @@ Use `--event <id>` to filter to a single event. Without it, commands search acro

## Behavior

- **Auto-refresh**: on first search, the CLI fetches and caches automatically. No explicit `refresh` needed.
- **Cache TTL**: 24 hours. `refresh --force` bypasses.
- **Auto-refresh**: search and lookup commands are cache-first. Missing caches are fetched automatically, and existing caches are revalidated only when their next check is due.
- **Revalidation**: due caches use conditional GET (ETag/Last-Modified). A 304 response avoids downloading the catalog body; network failures fall back to stale cache.
- **Network-friendly checks**: recent checks are skipped, stable catalogs are checked less often, and failed checks use backoff with jitter to avoid request spikes.
- **Disambiguation**: if a session code exists in multiple events, the CLI shows options.
- **Results**: 10 by default, `--limit` to override.

Expand Down
62 changes: 52 additions & 10 deletions cli/src/commands/common.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
import { KNOWN_EVENTS } from '../config.js';
import { getAllCachedSessions, fetchAndCache } from '../data/cache.js';
import {
fetchAndCache,
getAllCachedSessions,
isCacheCheckDue,
readMeta,
readSessions,
} from '../data/cache.js';
import type { Session } from '../contracts.js';
import { FetchError } from '../errors.js';

export async function ensureCache(): Promise<void> {
const sessions = await getAllCachedSessions();
if (sessions.length === 0) {
process.stderr.write('No cached sessions. Fetching...\n');
for (const event of KNOWN_EVENTS) {
try {
export async function ensureCache(): Promise<Session[]> {
let missingCacheHeaderPrinted = false;
const availableSessions: Session[] = [];

for (const event of KNOWN_EVENTS) {
const cachedSessions = await readSessions(event.id);
const meta = await readMeta(event.id);
const isMissingCache = cachedSessions.length === 0;
Comment thread
TianqiZhang marked this conversation as resolved.
Comment thread
TianqiZhang marked this conversation as resolved.

if (!isMissingCache && !isCacheCheckDue(meta)) {
availableSessions.push(...cachedSessions);
continue;
}

try {
if (isMissingCache) {
if (!missingCacheHeaderPrinted) {
process.stderr.write('Fetching missing session caches...\n');
missingCacheHeaderPrinted = true;
}
process.stderr.write(` ${event.name}...`);
const fetched = await fetchAndCache(event);
}

const fetched = await fetchAndCache(event, {
cachedMeta: meta,
cachedSessions,
});
availableSessions.push(...fetched);
if (isMissingCache) {
process.stderr.write(` ${fetched.length} sessions.\n`);
} catch {
process.stderr.write(' unavailable.\n');
}
} catch (err) {
if (!(err instanceof FetchError)) {
throw err;
}

if (isMissingCache) {
process.stderr.write(` unavailable: ${err.message}\n`);
} else {
availableSessions.push(...cachedSessions);
process.stderr.write(`Could not refresh ${event.name}; using cached sessions.\n`);
}
}
}

return availableSessions.length > 0
? availableSessions
: getAllCachedSessions();
}
10 changes: 7 additions & 3 deletions cli/src/commands/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ export async function refresh(eventFilter?: string, force: boolean = false): Pro

for (const event of events) {
try {
process.stderr.write(`Fetching ${event.name}...`);
const sessions = await fetchAndCache(event, force);
process.stderr.write(` ${sessions.length} sessions cached.\n`);
process.stderr.write(`Checking ${event.name}...\n`);
await fetchAndCache(event, {
force,
log: (message) => {
process.stderr.write(message);
},
});
Comment thread
TianqiZhang marked this conversation as resolved.
} catch (err) {
if (err instanceof FetchError) {
process.stderr.write(` failed: ${err.message}\n`);
Expand Down
4 changes: 1 addition & 3 deletions cli/src/commands/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getAllCachedSessions } from '../data/cache.js';
import { buildIndex, findSession } from '../search/index.js';
import { formatSessionDetail } from '../output/format.js';
import { ensureCache } from './common.js';
Expand All @@ -7,8 +6,7 @@ export async function session(
code: string,
opts: { event?: string; json?: boolean },
): Promise<void> {
await ensureCache();
const all = await getAllCachedSessions();
const all = await ensureCache();
buildIndex(all);

const matches = findSession(code, opts.event);
Expand Down
4 changes: 1 addition & 3 deletions cli/src/commands/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { getAllCachedSessions } from '../data/cache.js';
import { buildIndex, searchSessions, type SearchOptions } from '../search/index.js';
import { formatSearchResults } from '../output/format.js';
import { ensureCache } from './common.js';

export async function sessions(opts: SearchOptions & { json?: boolean }): Promise<void> {
await ensureCache();
const all = await getAllCachedSessions();
const all = await ensureCache();
buildIndex(all);

const results = searchSessions(opts);
Expand Down
16 changes: 15 additions & 1 deletion cli/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export interface RawSession {
TimeSlot?: string;
startDateTime?: string;
endDateTime?: string;
location?: string;
location?: Array<{ displayValue?: string; logicalValue?: string } | string>
| { displayValue?: string; logicalValue?: string }
| string;
sessionLevel?: Array<{ displayValue?: string; logicalValue?: string }> | string;
sessionType?: { displayValue?: string; logicalValue?: string } | string;
topic?: Array<{ displayValue?: string; logicalValue?: string }> | string;
Expand Down Expand Up @@ -50,12 +52,24 @@ export interface EventConfig {
endpoint: string;
}

export type CacheCheckStatus = 'updated' | 'not-modified' | 'failed';

export interface CacheMeta {
eventId: string;
/**
* When session content was last downloaded and written locally.
* Kept as fetchedAt for compatibility with existing cache metadata.
*/
fetchedAt: string;
/** Last time the remote catalog was checked, including 304 responses. */
checkedAt?: string;
/** Next time search commands may revalidate this cache. */
nextCheckAt?: string;
sessionCount: number;
etag?: string;
lastModified?: string;
lastCheckStatus?: CacheCheckStatus;
consecutiveFailures?: number;
}

export interface SearchResult {
Expand Down
Loading
Loading