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
22 changes: 2 additions & 20 deletions apps/world-lab/src/app/maplibre-worker/[file]/route.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,9 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { notFound } from 'next/navigation';

const mapLibreWorkerFiles = new Set([
'maplibre-gl-shared.mjs',
'maplibre-gl-worker.mjs',
]);
import { serveMapLibreWorkerFile } from '../serve-worker-file';

export async function GET(
_request: Request,
context: { params: Promise<{ file: string }> },
) {
const { file } = await context.params;
if (!mapLibreWorkerFiles.has(file)) notFound();

const contents = await readFile(
join(process.cwd(), 'node_modules', 'maplibre-gl', 'dist', file),
);

return new Response(contents, {
headers: {
'Cache-Control': 'public, max-age=31536000, immutable',
'Content-Type': 'text/javascript; charset=utf-8',
},
});
return serveMapLibreWorkerFile(file);
}
51 changes: 51 additions & 0 deletions apps/world-lab/src/app/maplibre-worker/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getVersion } from 'maplibre-gl';
import { describe, expect, it } from 'vitest';
import { GET as getLegacyWorker } from './[file]/route';
import { GET as getVersionedWorker } from './v/[version]/[file]/route';

describe('MapLibre worker routes', () => {
it('serves versioned allowlisted files with immutable caching', async () => {
const response = await getVersionedWorker(new Request('http://localhost'), {
params: Promise.resolve({
version: getVersion(),
file: 'maplibre-gl-worker.mjs',
}),
});

expect(response.status).toBe(200);
expect(response.headers.get('Cache-Control')).toBe(
'public, max-age=31536000, immutable',
);
expect(await response.text()).toContain('maplibre-gl-shared.mjs');
});

it('returns not found for stale versions and files outside the allowlist', async () => {
await expect(
getVersionedWorker(new Request('http://localhost'), {
params: Promise.resolve({
version: 'stale-version',
file: 'maplibre-gl-worker.mjs',
}),
}),
).rejects.toThrow('NEXT_HTTP_ERROR_FALLBACK;404');

await expect(
getVersionedWorker(new Request('http://localhost'), {
params: Promise.resolve({
version: getVersion(),
file: 'unlisted.mjs',
}),
}),
).rejects.toThrow('NEXT_HTTP_ERROR_FALLBACK;404');
});

it('serves legacy URLs without allowing browsers to retain stale bundles', async () => {
const response = await getLegacyWorker(new Request('http://localhost'), {
params: Promise.resolve({ file: 'maplibre-gl-shared.mjs' }),
});

expect(response.status).toBe(200);
expect(response.headers.get('Cache-Control')).toBe('no-store');
expect(await response.text()).toContain('maplibre-gl-shared.mjs.map');
});
});
38 changes: 38 additions & 0 deletions apps/world-lab/src/app/maplibre-worker/serve-worker-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { notFound } from 'next/navigation';
import { mapLibreWorkerFiles } from './worker-files';

async function getInstalledMapLibreVersion() {
const packageContents = await readFile(
join(process.cwd(), 'node_modules', 'maplibre-gl', 'package.json'),
'utf8',
);
return (JSON.parse(packageContents) as { version: string }).version;
}

export async function serveMapLibreWorkerFile(
file: string,
requestedVersion?: string,
) {
if (!mapLibreWorkerFiles.has(file)) notFound();

if (requestedVersion !== undefined) {
const installedVersion = await getInstalledMapLibreVersion();
if (requestedVersion !== installedVersion) notFound();
}

const contents = await readFile(
join(process.cwd(), 'node_modules', 'maplibre-gl', 'dist', file),
);

return new Response(contents, {
headers: {
'Cache-Control':
requestedVersion === undefined
? 'no-store'
: 'public, max-age=31536000, immutable',
'Content-Type': 'text/javascript; charset=utf-8',
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { serveMapLibreWorkerFile } from '../../../serve-worker-file';

export async function GET(
_request: Request,
context: { params: Promise<{ version: string; file: string }> },
) {
const { file, version } = await context.params;
return serveMapLibreWorkerFile(file, version);
}
4 changes: 4 additions & 0 deletions apps/world-lab/src/app/maplibre-worker/worker-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const mapLibreWorkerFiles = new Set([
'maplibre-gl-shared.mjs',
'maplibre-gl-worker.mjs',
]);
3 changes: 2 additions & 1 deletion apps/world-lab/src/components/world-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import { cellToBoundary, cellToLatLng } from 'h3-js';
import {
AttributionControl,
getVersion,
LngLatBounds,
type GeoJSONSource,
Map,
Expand Down Expand Up @@ -43,7 +44,7 @@ const sourceId = 'development-hexes';
const fillLayerId = 'development-hex-fills';
const lineLayerId = 'development-hex-lines';

setWorkerUrl('/maplibre-worker/maplibre-gl-worker.mjs');
setWorkerUrl(`/maplibre-worker/v/${getVersion()}/maplibre-gl-worker.mjs`);

type OverlayStatus = 'initializing' | 'ready' | 'incomplete' | 'failed';

Expand Down
6 changes: 6 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ World Lab issues explicit ticks while Start or a bounded run is active. Provider

`apps/world-lab` is a Next.js App Router developer/admin surface. It fetches runtime-validated simulation snapshots through a local rewrite, controls one tick at a time, and updates MapLibre's existing H3 GeoJSON source without recreating the map. Agent markers are fully visible and use deterministic offsets when sharing cells.

MapLibre's worker URL includes the renderer's package version. The worker and
its relative shared-module import use the same versioned route, which serves
only the installed version and two allowlisted files. Versioned responses use
immutable caching; legacy unversioned responses use `no-store`. This keeps
dependency upgrades from pairing a new renderer with cached older workers.

Its command navbar is the single persistent application-control row. Browser-session run-target selection remains client orchestration and preserves absolute tick semantics; execution and reconciliation still consume authoritative API snapshots. Agent color uses retained effective color, base agent color, and a neutral fallback.

The basemap uses the standard HTTPS OpenStreetMap raster tile endpoint with
Expand Down
13 changes: 10 additions & 3 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,18 @@ neutral fallback for unknown agents.
OpenStreetMap tile URLs, visible attribution, source zoom bounds, and a dark
raster treatment that leaves domain overlays untouched.

`maplibre-worker/route.test.ts` covers matching package versions, rejected
versions and filenames, immutable versioned responses, and uncached legacy
responses.

### Playwright E2E (`tests/e2e/world-lab.spec.ts`)

Two tests: the long swarm-activity log scrolls inside the fixed-height bottom
dock; and a deterministic scripted swarm tick commits and exports safe
telemetry without an OpenRouter request. Browser tests fulfill OpenStreetMap
Three tests: a map renders using matching versioned worker assets even when
the legacy worker URL is intercepted with a stale response; the long
swarm-activity log scrolls inside the fixed-height bottom dock; and a
deterministic scripted swarm tick commits and exports safe telemetry without
an OpenRouter request. The worker regression also checks both versioned asset
responses and rejects a mismatched package version. Browser tests fulfill OpenStreetMap
tile requests with a local image fixture instead of contacting the public tile
service.

Expand Down
47 changes: 47 additions & 0 deletions tests/e2e/world-lab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,53 @@ async function openMoreActions(page: Parameters<typeof test>[0]['page']) {
}
}

test('renders the map without using stale unversioned worker assets', async ({
page,
context,
}) => {
const { dependencies } = JSON.parse(
await readFile('apps/world-lab/package.json', 'utf8'),
);
const version = dependencies['maplibre-gl'];
const workerRequests: string[] = [];
const errors: string[] = [];
context.on('request', (request) => {
if (request.url().includes('/maplibre-worker/')) {
workerRequests.push(new URL(request.url()).pathname);
}
});
page.on('pageerror', (error) => errors.push(error.message));
await context.route('**/maplibre-worker/maplibre-gl-*.mjs', (route) =>
route.fulfill({
contentType: 'text/javascript',
body: 'throw new Error("Stale unversioned MapLibre worker loaded");',
}),
);

await page.goto('/');
await expect(page.getByTestId('world-map')).toHaveAttribute(
'data-overlay-status',
'ready',
);
expect(workerRequests).toContain(
`/maplibre-worker/v/${version}/maplibre-gl-worker.mjs`,
);
expect(errors).toEqual([]);

for (const file of ['maplibre-gl-worker.mjs', 'maplibre-gl-shared.mjs']) {
const response = await context.request.get(
`/maplibre-worker/v/${version}/${file}`,
);
expect(response.ok()).toBe(true);
expect(response.headers()['cache-control']).toContain('immutable');
expect(await response.text()).toContain(`v${version}/LICENSE.txt`);
}
const mismatched = await context.request.get(
'/maplibre-worker/v/0.0.0/maplibre-gl-worker.mjs',
);
expect(mismatched.status()).toBe(404);
});

test('keeps long swarm activity scrollable inside the bottom dock', async ({
page,
}) => {
Expand Down
Loading