fix(server): plan 3h close-out — new-value-wins COALESCE restores stay/reservation propagation; doc-sync state filter truthiness; request-context and parity ratchets for share, doc-sync and the integrations

This commit is contained in:
jubnl
2026-09-24 02:39:30 +02:00
parent 915ab84faf
commit f337f116d9
14 changed files with 379 additions and 103 deletions
@@ -14,6 +14,7 @@ import {
Req,
UseGuards,
} from '@nestjs/common';
import { MikroORM } from '@mikro-orm/core';
import type { Request } from 'express';
import type { User } from '../../types';
import { ADDON_IDS } from '../../addons';
@@ -23,6 +24,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { TripAccessGuard } from '../permissions/trip-access.guard';
import { RealtimeService } from '../realtime/realtime.service';
import { withRequestContext } from '../database/request-context';
import { docFailed } from './document-provider';
import { DocSyncConfigService, type LinkRow } from './doc-sync-config.service';
import { DocSyncService } from './doc-sync.service';
@@ -70,6 +72,7 @@ export class DocSyncController {
private readonly sync: DocSyncService,
private readonly registry: DocumentProviderRegistry,
private readonly realtime: RealtimeService,
private readonly orm: MikroORM,
) {}
/**
@@ -316,7 +319,24 @@ export class DocSyncController {
// A first run right away, so the user sees something happen instead of
// waiting out a poll interval and wondering whether it worked.
void this.sync.syncLink(res.data, { full: true });
//
// L1 (Plan 3h Task 7 review) — a third R9-class detached chain the
// inventory missed: `syncLink` runs ~36 ORM/Kysely statements, detached
// after this response goes out, on the request's own EntityManager fork.
// Wrapped in its own `withRequestContext` fork for the same reason the
// webhook nudge timer (`DocSyncWebhookController#schedule`) and the
// import-job runner (`ImportJobsService#start`) are: insurance against
// the request-scoped fork being gone by the time this runs, per 3f's own
// measurement that `AsyncLocalStorage` survives a detached chain intact
// in this codebase today, not a fix for an observed failure. A failure
// here is not a failure of the binding (the next poll carries it), so it
// is logged rather than thrown — same posture as the webhook
// subscription failure just above.
void withRequestContext(this.orm, () => this.sync.syncLink(res.data, { full: true })).catch((err: unknown) => {
this.logger.error(
`link ${res.data.id}: the first run after creation failed (${err instanceof Error ? err.message : String(err)}), the next poll carries it`,
);
});
return await this.config.publicLink((await this.config.getLink(res.data.id)) ?? res.data, publicOrigin(req));
}
+6 -1
View File
@@ -979,9 +979,14 @@ export class DocSyncService {
* queries (a 3rd near-duplicate listing shape alongside {@link issues}'s
* own `LIMIT 200` fixed-state-list) stay two repository methods, per R2's
* own instruction not to unify near-duplicate reads with different shapes.
* `state` is checked for TRUTHINESS, not `undefined` (Plan 3h Task 7
* review, M2): legacy's own gate (`doc-sync.controller.ts`, pre-R2) read
* `state ? <filtered> : <unfiltered>`, so `?state=` (an empty string)
* returns every item — an `=== undefined` check would instead filter on
* `state = ''`, which matches nothing.
*/
async itemsForTrip(tripId: number, state?: string) {
return state === undefined ? this.items.listForTrip(tripId) : this.items.listForTripByState(tripId, state);
return state ? this.items.listForTripByState(tripId, state) : this.items.listForTrip(tripId);
}
/** Per-trip view for the UI: what is synced, what needs attention. */