Skip to content

Commit 68fc53b

Browse files
committed
[codex] Add DWG and IFC derivative cache pipeline
1 parent 13776b2 commit 68fc53b

14 files changed

Lines changed: 1607 additions & 52 deletions

File tree

03-frontend/app/api/local-files/[fileId]/cad-derivative/route.ts

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,44 @@ export async function GET(
2222

2323
try {
2424
if (format === 'manifest') {
25-
return NextResponse.json(await buildCadDerivativeManifest(fileId));
25+
const manifest = await buildCadDerivativeManifest(fileId);
26+
if (request.headers.get('if-none-match') === manifest.etag) {
27+
return new Response(null, {
28+
status: 304,
29+
headers: manifestHeaders(manifest),
30+
});
31+
}
32+
return NextResponse.json(manifest, { headers: manifestHeaders(manifest) });
2633
}
2734

2835
const derivative = await readCadDerivativeBytes(fileId, format, sheet);
29-
const body = new Uint8Array(derivative.bytes.byteLength);
30-
body.set(derivative.bytes);
36+
if (request.headers.get('if-none-match') === derivative.etag) {
37+
return new Response(null, {
38+
status: 304,
39+
headers: derivativeHeaders(derivative),
40+
});
41+
}
42+
43+
const range = parseRangeHeader(
44+
request.headers.get('range'),
45+
derivative.bytes.byteLength,
46+
);
47+
const payload = range
48+
? derivative.bytes.subarray(range.start, range.end + 1)
49+
: derivative.bytes;
50+
const body = new Uint8Array(payload.byteLength);
51+
body.set(payload);
3152
return new Response(body, {
53+
status: range ? 206 : 200,
3254
headers: {
33-
'content-type': derivative.mediaType,
34-
'content-length': String(derivative.bytes.byteLength),
55+
...derivativeHeaders(derivative),
56+
'content-length': String(payload.byteLength),
3557
'content-disposition': `inline; filename*=UTF-8''${encodeURIComponent(derivative.fileName)}`,
36-
'x-architoken-cad-engine': derivative.engine,
58+
...(range
59+
? {
60+
'content-range': `bytes ${range.start}-${range.end}/${derivative.bytes.byteLength}`,
61+
}
62+
: {}),
3763
},
3864
});
3965
} catch (error) {
@@ -63,3 +89,50 @@ function normalizeFormat(value: string | null): CadDerivativeFormat {
6389
}
6490
return 'manifest';
6591
}
92+
93+
function manifestHeaders(manifest: { etag: string; fileId: string }) {
94+
return {
95+
etag: manifest.etag,
96+
'cache-control': 'private, max-age=0, must-revalidate',
97+
'x-architoken-file-id': manifest.fileId,
98+
'x-architoken-cache-contract': 'stream+etag+checksum',
99+
};
100+
}
101+
102+
function derivativeHeaders(derivative: {
103+
mediaType: string;
104+
engine: string;
105+
etag: string;
106+
cacheHit: boolean;
107+
}) {
108+
return {
109+
'content-type': derivative.mediaType,
110+
etag: derivative.etag,
111+
'cache-control': 'private, max-age=0, must-revalidate',
112+
'accept-ranges': 'bytes',
113+
'x-architoken-cad-engine': derivative.engine,
114+
'x-architoken-cache-hit': String(derivative.cacheHit),
115+
};
116+
}
117+
118+
function parseRangeHeader(
119+
header: string | null,
120+
size: number,
121+
): { start: number; end: number } | null {
122+
if (!header?.startsWith('bytes=')) {
123+
return null;
124+
}
125+
const [startRaw, endRaw] = header.slice('bytes='.length).split('-', 2);
126+
const start = Number.parseInt(startRaw ?? '', 10);
127+
const requestedEnd = Number.parseInt(endRaw ?? '', 10);
128+
if (!Number.isFinite(start) || start < 0 || start >= size) {
129+
return null;
130+
}
131+
const end = Number.isFinite(requestedEnd)
132+
? Math.min(requestedEnd, size - 1)
133+
: size - 1;
134+
if (end < start) {
135+
return null;
136+
}
137+
return { start, end };
138+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// app/api/local-files/[fileId]/ifc-derivative/route.ts - IFC derivative cache endpoint
2+
// License: Apache-2.0
3+
4+
import { NextResponse } from 'next/server';
5+
import {
6+
buildIfcDerivativeManifest,
7+
IfcDerivativeError,
8+
readIfcDerivativeBytes,
9+
type IfcDerivativeFormat,
10+
} from '@/lib/ifc-derivative-server';
11+
12+
export const runtime = 'nodejs';
13+
14+
export async function GET(
15+
request: Request,
16+
{ params }: { params: Promise<{ fileId: string }> },
17+
) {
18+
const { fileId } = await params;
19+
const url = new URL(request.url);
20+
const format = normalizeFormat(url.searchParams.get('format'));
21+
22+
try {
23+
if (format === 'manifest') {
24+
const manifest = await buildIfcDerivativeManifest(fileId);
25+
if (request.headers.get('if-none-match') === manifest.etag) {
26+
return new Response(null, {
27+
status: 304,
28+
headers: cacheHeaders(manifest.etag, manifest.fileId),
29+
});
30+
}
31+
return NextResponse.json(manifest, {
32+
headers: cacheHeaders(manifest.etag, manifest.fileId),
33+
});
34+
}
35+
36+
const derivative = await readIfcDerivativeBytes(fileId, format);
37+
if (request.headers.get('if-none-match') === derivative.etag) {
38+
return new Response(null, {
39+
status: 304,
40+
headers: derivativeHeaders(derivative),
41+
});
42+
}
43+
44+
const range = parseRangeHeader(
45+
request.headers.get('range'),
46+
derivative.bytes.byteLength,
47+
);
48+
const payload = range
49+
? derivative.bytes.subarray(range.start, range.end + 1)
50+
: derivative.bytes;
51+
const body = new Uint8Array(payload.byteLength);
52+
body.set(payload);
53+
return new Response(body, {
54+
status: range ? 206 : 200,
55+
headers: {
56+
...derivativeHeaders(derivative),
57+
'content-length': String(payload.byteLength),
58+
'content-disposition': `inline; filename*=UTF-8''${encodeURIComponent(derivative.fileName)}`,
59+
...(range
60+
? {
61+
'content-range': `bytes ${range.start}-${range.end}/${derivative.bytes.byteLength}`,
62+
}
63+
: {}),
64+
},
65+
});
66+
} catch (error) {
67+
if (error instanceof IfcDerivativeError) {
68+
return NextResponse.json(
69+
{
70+
error: error.code,
71+
message: error.message,
72+
...error.details,
73+
},
74+
{ status: error.status },
75+
);
76+
}
77+
return NextResponse.json(
78+
{
79+
error: 'ifc_derivative_failed',
80+
message: error instanceof Error ? error.message : String(error),
81+
},
82+
{ status: 500 },
83+
);
84+
}
85+
}
86+
87+
function normalizeFormat(value: string | null): IfcDerivativeFormat {
88+
if (value === 'properties-index') {
89+
return value;
90+
}
91+
return 'manifest';
92+
}
93+
94+
function cacheHeaders(etag: string, fileId: string) {
95+
return {
96+
etag,
97+
'cache-control': 'private, max-age=0, must-revalidate',
98+
'x-architoken-file-id': fileId,
99+
'x-architoken-cache-contract': 'stream+etag+checksum',
100+
};
101+
}
102+
103+
function derivativeHeaders(derivative: {
104+
mediaType: string;
105+
etag: string;
106+
cacheHit: boolean;
107+
}) {
108+
return {
109+
'content-type': derivative.mediaType,
110+
etag: derivative.etag,
111+
'cache-control': 'private, max-age=0, must-revalidate',
112+
'accept-ranges': 'bytes',
113+
'x-architoken-cache-hit': String(derivative.cacheHit),
114+
};
115+
}
116+
117+
function parseRangeHeader(
118+
header: string | null,
119+
size: number,
120+
): { start: number; end: number } | null {
121+
if (!header?.startsWith('bytes=')) {
122+
return null;
123+
}
124+
const [startRaw, endRaw] = header.slice('bytes='.length).split('-', 2);
125+
const start = Number.parseInt(startRaw ?? '', 10);
126+
const requestedEnd = Number.parseInt(endRaw ?? '', 10);
127+
if (!Number.isFinite(start) || start < 0 || start >= size) {
128+
return null;
129+
}
130+
const end = Number.isFinite(requestedEnd)
131+
? Math.min(requestedEnd, size - 1)
132+
: size - 1;
133+
if (end < start) {
134+
return null;
135+
}
136+
return { start, end };
137+
}

03-frontend/app/api/local-files/[fileId]/native-open/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ function nativeRoutesFor(ext: string, sourceUrl: string) {
135135
},
136136
{
137137
id: 'ifc-worker-cache',
138-
status: 'ready_in_worker_contract',
138+
status: 'ready',
139139
worker: 'IfcOpenShell / ThatOpen fragments',
140+
manifestUrl: `${sourceUrl}/ifc-derivative?format=manifest`,
141+
propertiesIndexUrl: `${sourceUrl}/ifc-derivative?format=properties-index`,
140142
outputs: ['glb', 'fragments', 'tiles', 'properties-index'],
141143
cache: 'checksum-keyed derivatives + paginated properties index',
142144
},

03-frontend/lib/cad-derivative-server.test.ts

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)