Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/eight-flies-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'squareone': patch
---

Migrate `TimesSquareHtmlEventsProviderClient` to `subscribeToHtmlEvents()` from `@lsst-sqre/times-square-client`, removing the duplicated inline SSE implementation and the local `HtmlEvent` type. The app's Times Square SSE path now gains Zod validation of events, `credentials: 'include'` (Gafaelfawr same-origin cookie auth), and automatic reconnection with backoff from the shared `@lsst-sqre/sse-client` transport. This removes `@microsoft/fetch-event-source` from `apps/squareone` — the dependency is now gone from the entire repo.
5 changes: 5 additions & 0 deletions .changeset/spicy-donuts-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lsst-sqre/times-square-client': minor
---

Rewrite the Times Square SSE layer (`subscribeToHtmlEvents`) on the shared `@lsst-sqre/sse-client` transport, replacing the unmaintained `@microsoft/fetch-event-source` dependency. The public API is unchanged — `subscribeToHtmlEvents()`, `createHtmlEventsUrl()`, `SubscribeOptions`, `HtmlEventCallback`, and `SseErrorCallback` keep their exact signatures — and Zod validation of `HtmlEvent`, auto-abort on execution completion, `onComplete`, and the optional structured `Logger` all remain in this layer. Reconnect policy now follows the transport's defaults: automatic reconnection with backoff, honoring server `retry:` fields, including after HTTP 4xx responses. Connection interruptions are surfaced through `onError`, and scheduled reconnects are logged through the `Logger` when provided.
6 changes: 6 additions & 0 deletions .changeset/tidy-moons-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lsst-sqre/times-square-client': patch
'squareone': patch
---

Fix Times Square dev mock API routes that had drifted from the real API: leaf nodes in the GitHub contents-tree mocks now include the required empty `contents` array, and the page-metadata mocks return the full `Page` shape (formatted-text `description`, `date_added`, `uploader_username`, `html_events_url`, `github`). New schema-conformance tests parse every JSON mock route with the Zod schemas from `@lsst-sqre/times-square-client`, and the Times Square v0.24.0 OpenAPI spec is vendored in the package as the reference for those schemas.
8 changes: 8 additions & 0 deletions .changeset/tidy-pears-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@lsst-sqre/sse-client': minor
---

Add the @lsst-sqre/sse-client package, the monorepo's shared Server-Sent Events (SSE) transport built on `eventsource-client`. The package follows the no-build-step pattern (consumers transpile TypeScript source directly) and exports:

- `subscribeToEventSource(url, options)` — a framework-agnostic subscribe function with raw `onMessage`/`onConnect`/`onDisconnect`/`onScheduleReconnect` callbacks, custom `headers`, `credentials` (defaulting to `'include'` for Gafaelfawr same-origin cookie auth), an external `AbortSignal`, and a `fetch` override. It returns a cleanup function that closes the connection and stops automatic reconnection. Payload parsing/validation is deliberately left to consumers.
- `useEventSource(url, options)` — a thin React hook wrapping the subscribe function with effect lifecycle management. The connection follows the component lifecycle and the `url` (pass `null` to disable); callbacks are read through a ref so identity changes never reconnect.
1 change: 1 addition & 0 deletions apps/squareone/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = (phase) => {
'@lsst-sqre/repertoire-client',
'@lsst-sqre/gafaelfawr-client',
'@lsst-sqre/semaphore-client',
'@lsst-sqre/sse-client',
'@lsst-sqre/times-square-client',
],
async rewrites() {
Expand Down
1 change: 0 additions & 1 deletion apps/squareone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"@lsst-sqre/semaphore-client": "workspace:*",
"@lsst-sqre/squared": "workspace:*",
"@lsst-sqre/times-square-client": "workspace:*",
"@microsoft/fetch-event-source": "^2.0.1",
"@sentry/nextjs": "^10.58.0",
"@tanstack/react-query": "^5.90.20",
"ajv": "^8.18.0",
Expand Down
106 changes: 106 additions & 0 deletions apps/squareone/src/app/api/dev/times-square/schema-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Schema conformance tests for the Times Square dev mock API routes.
*
* Each mock route's payload is parsed with the corresponding Zod schema from
* @lsst-sqre/times-square-client (which mirrors the Times Square OpenAPI spec
* vendored at packages/times-square-client/openapi.json), so drift between the
* mocks and the real API fails here instead of as a runtime ZodError in dev.
*/

import {
GitHubContentsRootSchema,
GitHubPrContentsSchema,
HtmlStatusSchema,
PageSchema,
PageSummarySchema,
} from '@lsst-sqre/times-square-client';
import { describe, expect, it, vi } from 'vitest';
import { GET as getGithubPage } from './v1/github/[...tsSlug]/route.dev';
import { GET as getGithubContents } from './v1/github/route.dev';
import { GET as getGithubPrPage } from './v1/github-pr/[owner]/[repo]/[commit]/[...tsSlug]/route.dev';
import { GET as getGithubPrContents } from './v1/github-pr/[owner]/[repo]/[commit]/route.dev';
import { GET as getHtmlStatus } from './v1/pages/[page]/htmlstatus/route.dev';
import { GET as getPage } from './v1/pages/[page]/route.dev';
import { GET as getPages } from './v1/pages/route.dev';

vi.mock('@/lib/config/loader', () => ({
loadAppConfig: vi.fn().mockResolvedValue({
timesSquareUrl: 'http://localhost:3000/times-square/api',
}),
}));

vi.mock('@/lib/logger', () => {
const log = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
return { default: log, createRouteLogger: () => log };
});

type Parseable = {
safeParse: (data: unknown) => { success: boolean; error?: unknown };
};

/** Parse a route response with a schema, surfacing Zod issues on failure. */
async function expectConforms(response: Response, schema: Parseable) {
expect(response.status).toBe(200);
const payload = await response.json();
const result = schema.safeParse(payload);
expect(result.error).toBeUndefined();
}

describe('Times Square dev mock schema conformance', () => {
it('GET /v1/pages conforms to PageSummary[]', async () => {
const response = await getPages();
await expectConforms(response, PageSummarySchema.array());
});

it('GET /v1/pages/[page] conforms to PageSchema', async () => {
const response = await getPage(new Request('http://localhost/mock'), {
params: Promise.resolve({ page: 'mypage' }),
});
await expectConforms(response, PageSchema);
});

it('GET /v1/pages/[page]/htmlstatus conforms to HtmlStatusSchema', async () => {
const response = await getHtmlStatus(
new Request('http://localhost/mock?a=1'),
{ params: Promise.resolve({ page: 'mypage' }) }
);
await expectConforms(response, HtmlStatusSchema);
});

it('GET /v1/pages/[page]/htmlstatus (unavailable mode) conforms to HtmlStatusSchema', async () => {
const response = await getHtmlStatus(
new Request('http://localhost/mock?a=2'),
{ params: Promise.resolve({ page: 'mypage' }) }
);
await expectConforms(response, HtmlStatusSchema);
});

it('GET /v1/github conforms to GitHubContentsRootSchema', async () => {
const response = await getGithubContents();
await expectConforms(response, GitHubContentsRootSchema);
});

it('GET /v1/github/[...tsSlug] conforms to PageSchema', async () => {
const response = await getGithubPage(new Request('http://localhost/mock'), {
params: Promise.resolve({
tsSlug: ['lsst-sqre', 'times-square-demo', 'matplotlib', 'gaussian2d'],
}),
});
await expectConforms(response, PageSchema);
});

it('GET /v1/github-pr/[owner]/[repo]/[commit] conforms to GitHubPrContentsSchema', async () => {
const response = await getGithubPrContents();
await expectConforms(response, GitHubPrContentsSchema);
});

it('GET /v1/github-pr/[owner]/[repo]/[commit]/[...tsSlug] conforms to PageSchema', async () => {
const response = await getGithubPrPage();
await expectConforms(response, PageSchema);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* /times-square/v1/github-pr/:owner/:repo/:commit/:slug (App Router version)
*/

import type { Page } from '@lsst-sqre/times-square-client';
import { NextResponse } from 'next/server';

import { loadAppConfig } from '@/lib/config/loader';
Expand All @@ -19,15 +20,25 @@ export async function GET() {
const { timesSquareUrl } = appConfig;
const pageBaseUrl = `${timesSquareUrl}/v1/pages/${page}`;

const content = {
// Shape must conform to the PageSchema Zod schema in
// @lsst-sqre/times-square-client (packages/times-square-client/src/schemas.ts).
const content: Page = {
name: page,
title: `Title for ${page}`,
description: '<p>This is the description.</p>',
description: {
gfm: 'This is the description.',
html: '<p>This is the description.</p>',
},
date_added: '2024-01-15T10:00:00Z',
authors: [],
tags: [],
uploader_username: null,
self_url: pageBaseUrl,
source_url: `${pageBaseUrl}/source`,
rendered_url: `${pageBaseUrl}/rendered`,
html_url: `${pageBaseUrl}/html`,
html_status_url: `${pageBaseUrl}/htmlstatus`,
html_events_url: `${pageBaseUrl}/htmlevents`,
parameters: {
a: {
type: 'number',
Expand All @@ -40,6 +51,12 @@ export async function GET() {
description: 'A string.',
},
},
github: {
owner: 'lsst-sqre',
repository: 'times-square-demo',
source_path: `${page}.ipynb`,
sidecar_path: `${page}.yaml`,
},
};

return NextResponse.json(content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* /times-square/v1/github-pr/:owner/:repo/:commit (App Router version)
*/

import type { GitHubPrContents } from '@lsst-sqre/times-square-client';
import { NextResponse } from 'next/server';

export async function GET() {
const data = {
const data: GitHubPrContents = {
contents: [
{
node_type: 'owner',
Expand All @@ -17,6 +18,7 @@ export async function GET() {
node_type: 'page',
title: 'Demo',
path: 'demo',
contents: [],
},
{
node_type: 'directory',
Expand All @@ -27,6 +29,7 @@ export async function GET() {
node_type: 'page',
title: 'Gaussian 2D',
path: 'matplotlib/gaussian2d',
contents: [],
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Fixed URLs to use mock API endpoints
*/

import type { Page } from '@lsst-sqre/times-square-client';
import { NextResponse } from 'next/server';

import { loadAppConfig } from '@/lib/config/loader';
Expand Down Expand Up @@ -31,10 +32,19 @@ export async function GET(
return new Response(null, { status: 404 });
}

const content = {
// Shape must conform to the PageSchema Zod schema in
// @lsst-sqre/times-square-client (packages/times-square-client/src/schemas.ts).
const content: Page = {
name: page,
title: `Title for ${page}`,
description: '<p>This is the description.</p>',
description: {
gfm: 'This is the description.',
html: '<p>This is the description.</p>',
},
date_added: '2024-01-15T10:00:00Z',
authors: [],
tags: [],
uploader_username: null,
self_url: pageBaseUrl,
source_url: `${pageBaseUrl}/source`,
rendered_url: `${pageBaseUrl}/rendered`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* Mock Times Square API endpoint: /times-square/v1/github (App Router version)
*/

import type { GitHubContentsRoot } from '@lsst-sqre/times-square-client';
import { NextResponse } from 'next/server';

export async function GET() {
const content = {
const content: GitHubContentsRoot = {
contents: [
{
node_type: 'owner',
Expand All @@ -21,6 +22,7 @@ export async function GET() {
node_type: 'page',
title: 'Demo',
path: 'lsst-sqre/times-square-demo/demo',
contents: [],
},
{
node_type: 'directory',
Expand All @@ -31,6 +33,7 @@ export async function GET() {
node_type: 'page',
title: 'Gaussian 2D',
path: 'lsst-sqre/times-square-demo/matplotlib/gaussian2d',
contents: [],
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Mock Times Square API endpoint: /times-square/v1/pages/:page (App Router version)
*/

import type { Page } from '@lsst-sqre/times-square-client';
import { NextResponse } from 'next/server';

import { loadAppConfig } from '@/lib/config/loader';
Expand All @@ -24,15 +25,25 @@ export async function GET(
return new Response(null, { status: 404 });
}

const content = {
// Shape must conform to the PageSchema Zod schema in
// @lsst-sqre/times-square-client (packages/times-square-client/src/schemas.ts).
const content: Page = {
name: page,
title: `Title for ${page}`,
description: '<p>This is the description.</p>',
description: {
gfm: 'This is the description.',
html: '<p>This is the description.</p>',
},
date_added: '2024-01-15T10:00:00Z',
authors: [],
tags: [],
uploader_username: 'someuser',
self_url: pageBaseUrl,
source_url: `${pageBaseUrl}/source`,
rendered_url: `${pageBaseUrl}/rendered`,
html_url: `${pageBaseUrl}/html`,
html_status_url: `${pageBaseUrl}/htmlstatus`,
html_events_url: `${pageBaseUrl}/htmlevents`,
parameters: {
a: {
type: 'number',
Expand All @@ -45,6 +56,7 @@ export async function GET(
description: 'A string.',
},
},
github: null,
};

return NextResponse.json(content);
Expand Down
Loading
Loading