fix(ski-routing): keep one job's timeout to itself and bound the area refresh

The router thread now takes one job at a time from a queue kept on the
main thread, so a job's clock starts when the thread takes it up. A job
that times out ends only its own thread, and only while that thread is
still the current one; the jobs in line go to a fresh thread instead of
waiting out their own timers and killing the new thread in turn. A
thread that dies fails the job it held, and a thread that cannot start
fails each job on its own.

- the daily refresh fetches changed areas two at a time and stops once
  the TREK Places breaker opens; the rest keep their older copies
- the winter index cap is on the unpacked JSON fetch hands over, so it
  follows from the host's 8 MB gzip cap (64 MB) instead of matching it
- a build name is read to the rule a ski tour stores it by, so a build
  TREK could not save is refused when the winter layer is read
- an unused import in the piste router contract is gone
This commit is contained in:
Maurice
2026-10-20 21:06:48 +02:00
parent 2d0c0597b3
commit 1d72fbd25f
12 changed files with 245 additions and 47 deletions
+8
View File
@@ -266,4 +266,12 @@ describe('the winter layer of the Places host (SKI-ROUTE)', () => {
expect(winterIndexSchema.parse(index).areas[0]).toEqual(row);
expect(winterIndexSchema.safeParse({ ...index, areas: [{ ...row, id: 'x' }] }).success).toBe(false);
});
it('SKI-ROUTE-SCHEMA-011: refuses a build name a ski tour could not be saved with', () => {
const index = { build: '2026-11-02', format: 1, areas: [] };
for (const build of ['nightly build', '2026+1', 'a:b']) {
expect(winterIndexSchema.safeParse({ ...index, build }).success).toBe(false);
}
expect(winterIndexSchema.safeParse({ ...index, build: '2026-11-02_redo.2' }).success).toBe(true);
});
});
-1
View File
@@ -1,7 +1,6 @@
import {
SKI_DIFFICULTIES,
skiDisciplineSchema,
skiRouteBuildSchema,
skiRouteOptionsSchema,
skiTechniqueSchema,
} from '../tours/ski.schema';
+11 -2
View File
@@ -1,3 +1,5 @@
import { skiRouteBuildSchema } from '../tours/ski.schema';
import { z } from 'zod';
/**
@@ -10,6 +12,13 @@ import { z } from 'zod';
const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/);
/**
* A build is read to the same rule a ski tour stores it by, so a build TREK
* could not save with a tour is refused when the layer is read, not on the
* tour's next save.
*/
const winterBuildSchema = skiRouteBuildSchema;
/** The attribution block the winter layer carries; only the two lines TREK prints are required. */
export const winterAttributionSchema = z.object({
data: z.string(),
@@ -18,7 +27,7 @@ export const winterAttributionSchema = z.object({
export type WinterAttribution = z.infer<typeof winterAttributionSchema>;
export const winterMetaSchema = z.object({
build: z.string().min(1).max(40),
build: winterBuildSchema,
format: z.number().int(),
areas: z.number().int().nonnegative(),
indexSha: sha256Schema,
@@ -58,7 +67,7 @@ export const winterIndexRowSchema = z.object({
export type WinterIndexRow = z.infer<typeof winterIndexRowSchema>;
export const winterIndexSchema = z.object({
build: z.string().min(1).max(40),
build: winterBuildSchema,
format: z.number().int(),
areas: z.array(winterIndexRowSchema),
});