@_linked/server 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/esm/backend.d.ts +6 -0
  3. package/lib/esm/backend.js +27 -0
  4. package/lib/esm/backend.js.map +1 -0
  5. package/lib/esm/index.d.ts +9 -0
  6. package/lib/esm/index.js +11 -0
  7. package/lib/esm/index.js.map +1 -0
  8. package/lib/esm/ontologies/lincd-server.d.ts +31 -0
  9. package/lib/esm/ontologies/lincd-server.js +42 -0
  10. package/lib/esm/ontologies/lincd-server.js.map +1 -0
  11. package/lib/esm/package.d.ts +4 -0
  12. package/lib/esm/package.js +3 -0
  13. package/lib/esm/package.js.map +1 -0
  14. package/lib/esm/shapes/LincdAPI.d.ts +41 -0
  15. package/lib/esm/shapes/LincdAPI.js +113 -0
  16. package/lib/esm/shapes/LincdAPI.js.map +1 -0
  17. package/lib/esm/shapes/LincdServer.d.ts +79 -0
  18. package/lib/esm/shapes/LincdServer.js +1257 -0
  19. package/lib/esm/shapes/LincdServer.js.map +1 -0
  20. package/lib/esm/shapes/LincdWebApp.d.ts +7 -0
  21. package/lib/esm/shapes/LincdWebApp.js +40 -0
  22. package/lib/esm/shapes/LincdWebApp.js.map +1 -0
  23. package/lib/esm/shapes/filestores/LocalFileStore.d.ts +42 -0
  24. package/lib/esm/shapes/filestores/LocalFileStore.js +93 -0
  25. package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -0
  26. package/lib/esm/shapes/quadstores/BackendAPIStore.d.ts +33 -0
  27. package/lib/esm/shapes/quadstores/BackendAPIStore.js +65 -0
  28. package/lib/esm/shapes/quadstores/BackendAPIStore.js.map +1 -0
  29. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.d.ts +13 -0
  30. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js +26 -0
  31. package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js.map +1 -0
  32. package/lib/esm/types/RouteConfig.d.ts +56 -0
  33. package/lib/esm/types/RouteConfig.js +2 -0
  34. package/lib/esm/types/RouteConfig.js.map +1 -0
  35. package/lib/esm/types.d.ts +12 -0
  36. package/lib/esm/types.js +1 -0
  37. package/lib/esm/types.js.map +1 -0
  38. package/lib/esm/utils/Shapes.d.ts +7 -0
  39. package/lib/esm/utils/Shapes.js +59 -0
  40. package/lib/esm/utils/Shapes.js.map +1 -0
  41. package/lib/esm/utils/accessUrl.d.ts +6 -0
  42. package/lib/esm/utils/accessUrl.js +9 -0
  43. package/lib/esm/utils/accessUrl.js.map +1 -0
  44. package/package.json +1 -1
@@ -0,0 +1,1257 @@
1
+ 'use strict';
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ import chalk from 'chalk';
12
+ import events from 'events';
13
+ import express from 'express';
14
+ import fetchCookie from 'fetch-cookie';
15
+ import * as fsNative from 'fs';
16
+ import * as fs from 'fs/promises';
17
+ import { getWebpackAppConfig } from '@_linked/cli/config-webpack-app';
18
+ import { getLincdPackages } from '@_linked/cli/cli-methods';
19
+ import { getPackageJSON } from '@_linked/cli/utils';
20
+ import { AppContextProvider } from '@_linked/server-utils/components/AppContext';
21
+ import { JSONParser } from '@_linked/server-utils/utils/JSONParser';
22
+ import { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';
23
+ import { Server } from '@_linked/server-utils/utils/Server';
24
+ import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
25
+ import { CoreMap } from '@_linked/core/collections/CoreMap';
26
+ import { Shape } from '@_linked/core/shapes/Shape';
27
+ import { LinkedErrorLogging } from '@_linked/core/utils/LinkedErrorLogging';
28
+ import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
29
+ import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
30
+ import { autoLoadOntologyData } from '@_linked/core/utils/Package';
31
+ import { getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
32
+ import path from 'path';
33
+ import process from 'process';
34
+ import * as React from 'react';
35
+ import { renderToPipeableStream, renderToStaticMarkup } from 'react-dom/server';
36
+ import { StaticRouter } from 'react-router-dom/server.js';
37
+ import { rimraf } from 'rimraf';
38
+ import sharp from 'sharp';
39
+ import { Transform } from 'stream';
40
+ import { CookieJar } from 'tough-cookie';
41
+ import webpack from 'webpack';
42
+ import webpackDevMiddleware from 'webpack-dev-middleware';
43
+ import webpackHotMiddleware from 'webpack-hot-middleware';
44
+ import { lincdServer } from '../ontologies/lincd-server.js';
45
+ import { linkedShape } from '../package.js';
46
+ import { syncShapes } from '../utils/Shapes.js';
47
+ import { LincdAPI } from './LincdAPI.js';
48
+ //prevent errors in node.js when (s)css files are imported in js
49
+ const isProduction = process.env.NODE_ENV === 'production';
50
+ const isDevelopment = process.env.NODE_ENV === 'development';
51
+ // Install a global fetch that persists cookies across redirects using a CookieJar.
52
+ // This ensures server-side requests behave more like browsers when handling Set-Cookie + redirects.
53
+ const __lincdCookieJar = new CookieJar();
54
+ // Wrap native fetch with fetch-cookie so it reads/writes cookies in the jar.
55
+ const __lincdFetchWithCookies = fetchCookie(fetch, __lincdCookieJar);
56
+ // Expose (optionally) for debugging/tests; not required by app logic.
57
+ globalThis.__lincdCookieJar = __lincdCookieJar;
58
+ // Set the global fetch used throughout the server code path
59
+ globalThis.fetch = __lincdFetchWithCookies;
60
+ process.on('uncaughtException', (err) => {
61
+ console.warn(chalk.red('Asynchronous error caught.'));
62
+ console.error(err);
63
+ // error logging
64
+ LinkedErrorLogging.log(err);
65
+ });
66
+ process.on('unhandledRejection', (err) => {
67
+ console.warn(chalk.red('Unhandled rejection caught.'));
68
+ console.error(err);
69
+ // error logging
70
+ LinkedErrorLogging.log(err);
71
+ });
72
+ process.on('warning', (e) => console.warn(e.stack));
73
+ //allow more listeners for when we have many concurrent users
74
+ events.EventEmitter.prototype.setMaxListeners(500);
75
+ // const jsdom = require("jsdom");
76
+ // const { JSDOM } = jsdom;
77
+ // const { document } = (new JSDOM(`...`)).window;
78
+ //
79
+ // global['document'] = document;
80
+ global['reactStaticRenderer'] = renderToStaticMarkup;
81
+ autoLoadOntologyData(true);
82
+ let LincdServer = class LincdServer extends Shape {
83
+ /**
84
+ * yarn linked start sends the contents of linked.config.js as an object to this constructor
85
+ * @param n
86
+ */
87
+ constructor(config) {
88
+ super(typeof config === 'string' || (config && 'id' in config)
89
+ ? config
90
+ : undefined);
91
+ this.cachedPaths = new Map();
92
+ this.latestManifest = null;
93
+ this.cssMode = 'scss-modules';
94
+ this.analyse = false;
95
+ this.shapeProviders = new CoreMap();
96
+ this.genericProviders = new CoreMap();
97
+ //from resizedFileName to full resized path (CDN or similar)
98
+ this.resizePathsMap = new Map();
99
+ if (config && typeof config !== 'string' && !('id' in config)) {
100
+ this.config = config;
101
+ }
102
+ this.api = new LincdAPI({ id: process.env.SITE_ROOT + '/api' });
103
+ //Ensure the Server utility (for Server.call()) directly accesses
104
+ // this server on the backend instead of going through a network call
105
+ Server.setLocalServer(this);
106
+ }
107
+ /*async getLincdDependencies(): Promise<string[]> {
108
+ let lincdDependencies = [];
109
+ let dependencies = this.package.dependencies;
110
+
111
+ await Promise.all(
112
+ Object.keys(dependencies).map((dependencyPkgName) => {
113
+ let modulePackageJson = getModulePackageJSON(dependencyPkgName);
114
+ if (modulePackageJson['lincd']) {
115
+ lincdDependencies.push(modulePackageJson.name);
116
+ }
117
+ // //TODO: also iteratively look into dependencies of this dependency
118
+
119
+ // let packagePath;
120
+ // try {
121
+ // packagePath = require.resolve(`${dependencyPkgName}`);
122
+ // } catch (err) {
123
+ // console.warn('Could not find package ' + dependencyPkgName+'. Error: '+err.toString());
124
+ // return;
125
+ // }
126
+ // packagePath = path.dirname(packagePath) + '/package.json'
127
+ // return fs
128
+ // .readFile(packagePath, 'utf-8')
129
+ // .then((res) => {
130
+ // let pkg = JSON.parse(res);
131
+ // if (pkg['lincd']) {
132
+ // lincdDependencies.push(pkg.name);
133
+ // }
134
+ // //TODO: also iteratively look into dependencies of this dependency
135
+ // })
136
+ // .catch((err) => {
137
+ // console.log('Could not read package.json file: '+err);
138
+ // });
139
+ }),
140
+ );
141
+ return lincdDependencies;
142
+ }*/
143
+ get app() {
144
+ return this.server;
145
+ }
146
+ initPackage() {
147
+ this.package = JSON.parse(fsNative.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8'));
148
+ }
149
+ async initOnly() {
150
+ await this.initOntologies();
151
+ await this.initStores();
152
+ this.initPackage();
153
+ this.server = express();
154
+ await this.initBackendProviders();
155
+ syncShapes();
156
+ return this;
157
+ }
158
+ // async serveData(req,res) {
159
+ // let nodeURI = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
160
+ //
161
+ // let store = LinkedStorage.getDatasets().find(store => {
162
+ // return nodeURI.includes(store.namedNode.uri)
163
+ // })
164
+ // }
165
+ async start() {
166
+ this.initPackage();
167
+ // Static assets should come from the static store URL (versioned path),
168
+ // not the upload store URL. storage-config sets STATIC_ACCESS_URL accordingly.
169
+ // In development we deliberately use a relative path so bundle URLs work
170
+ // regardless of which PORT the dev server bound to (browsers resolve
171
+ // relative URLs against window.location.origin). Hardcoding SITE_ROOT
172
+ // baked :4000 into every SSR'd HTML page.
173
+ const isDevAssets = process.env.NODE_ENV === 'development';
174
+ const staticAccessURL = isDevAssets
175
+ ? ''
176
+ : (process.env.STATIC_ACCESS_URL ||
177
+ LinkedFileStorage.accessURL ||
178
+ '').replace(/\/$/, '');
179
+ const staticAsset = (assetPath) => `${staticAccessURL}/public${assetPath}`;
180
+ //for apps with multiple bundles this should be read from the webpack build manifest
181
+ this.assets = {
182
+ 'main.js': staticAsset('/bundles/main.bundle.js') + '?v=' + this.package.version, //output js bundle from Webpack
183
+ 'main.css': staticAsset('/bundles/main.css'), //output css from Webpack
184
+ };
185
+ try {
186
+ const manifestPath = path.resolve(process.cwd(), 'public/bundles/manifest.json');
187
+ if (fsNative.existsSync(manifestPath)) {
188
+ const manifestRaw = fsNative.readFileSync(manifestPath, 'utf-8');
189
+ this.assets.manifest = JSON.parse(manifestRaw);
190
+ }
191
+ }
192
+ catch (err) {
193
+ console.warn('Could not load bundle manifest:', err);
194
+ }
195
+ const isProduction = process.env.NODE_ENV === 'production';
196
+ if (!isProduction && this.config.cssMode === 'tailwind') {
197
+ this.assets['tailwind-cdn'] = 'https://cdn.tailwindcss.com';
198
+ }
199
+ //for multicore we use PM2, and each instance needs to listen to port 0.
200
+ // whilst the main thread will listen to env.PORT automatically
201
+ // const PORT = this.config.multiCore ? 0 : process.env.PORT || 3000;
202
+ // const publicPort = process.env.PORT || 3000;
203
+ //update: back to original setup. multicore handles itself inside @semantu/multicore
204
+ const PORT = parseInt(process.env.PORT) || 4000;
205
+ this.server = express();
206
+ const dirName = path.resolve(process.cwd(), 'frontend');
207
+ await this.initOntologies();
208
+ //TODO: when we do not keep all data in memory (and thus do not need to rely on in memory data for page requests), we can possibly remove await here, since all stores handle their own initialisation before executing commands
209
+ await this.initStores();
210
+ this.initGarbageCollection();
211
+ // this.app.use((req, res, next) => {
212
+ // console.log('start request');
213
+ // next();
214
+ // });
215
+ // before controllers
216
+ await this.initBackendProviders();
217
+ syncShapes();
218
+ //START OF EXPRESS ROUTES AND MIDDLEWARE
219
+ //use cors
220
+ // var corsOptions = {
221
+ // origin: ['http://localhost:4001', 'https://www.mynd.site'],
222
+ // optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
223
+ // };
224
+ // //accept JSON bodies
225
+ // this.server.use(bodyParser.json({limit: '50mb'}));
226
+ this.server.use(express.json({ limit: '50mb' }));
227
+ // this.server.use(cors(corsOptions));
228
+ //
229
+ // //compress server output with gzip,
230
+ //UPDATE ive set level to 2 (low compression fast speed) because responses were taking too long (98% of server time was used by compress)
231
+ // this.server.use(compress({level: 2}));
232
+ await this.callGenericBackendProvidersMethod('setupBeforeControllers');
233
+ // if development, run webpack from the server
234
+ // for production you need to build the bundles before starting the server
235
+ const skipBuild = process.env.NO_WEBPACK === 'true';
236
+ if (isDevelopment && !skipBuild) {
237
+ //three levels up because of lib/esm (2) instead of src (1)
238
+ //@ts-ignore
239
+ // const getWebpackConfig = (await import('../../../site.webpack.config.cjs')).default;
240
+ // let webpackConfig = await getWebpackConfig();
241
+ let webpackConfig = await getWebpackAppConfig();
242
+ // const compare = (c1,c2,path?='') => {
243
+ // for(let key1 of Object.keys(c1)) {
244
+ // if(typeof c1[key1] === 'object') {
245
+ // compare(c1[key1],c2[key1],path+='.'+key1);
246
+ // } else {
247
+ // if(c1[key1] !== c2[key1]) {
248
+ // console.log("Difference "+path+": ");
249
+ // console.log(c1[key1]);
250
+ // console.log(c2[key1]);
251
+ // }
252
+ // }
253
+ // }
254
+ // }
255
+ // compare(webpackConfig,webpackConfig2);
256
+ //default to having webpack cache on and filesystem, unless explicitly set to false
257
+ if (this.cacheWebpack === false) {
258
+ webpackConfig.cache.type = 'memory';
259
+ }
260
+ else {
261
+ webpackConfig.cache.type = 'filesystem';
262
+ }
263
+ //clean dist/build folder
264
+ await rimraf(webpackConfig.output.path).catch((err) => {
265
+ if (err) {
266
+ console.warn(err);
267
+ }
268
+ });
269
+ const compiler = webpack(webpackConfig, (err, stats) => {
270
+ //watch build completed
271
+ if (err) {
272
+ console.error(err);
273
+ }
274
+ // Output stats JSON for analysis
275
+ if (this.analyse) {
276
+ fs.writeFile('./data/webpack-stats.json', JSON.stringify(stats.toJson({ all: true }), null, 2)).then(() => {
277
+ console.log('Webpack stats written to ./data/webpack-stats.json');
278
+ });
279
+ }
280
+ });
281
+ if (!compiler) {
282
+ //something went wrong with webpack config, error will be logged above
283
+ return;
284
+ }
285
+ compiler.hooks.afterEmit.tap('cleanup-the-require-cache', () => {
286
+ // After webpack rebuild, clear the files from the require cache,
287
+ // so that next server side render wil be in sync
288
+ // console.log(Object.keys(require.cache).filter(k => k.includes(dirName)).join("\n"));
289
+ // if (typeof require !== 'undefined') {
290
+ // Object.keys(require.cache)
291
+ // .filter((key) => key.includes(dirName))
292
+ // .forEach((key) => delete require.cache[key]);
293
+ // }
294
+ });
295
+ //after the first emit (which means bundles are ready and the site is running) also rebuild the index files of the site
296
+ let updatedMetadata = false;
297
+ compiler.hooks.afterEmit.tap('update-metadata', () => {
298
+ if (!updatedMetadata) {
299
+ updatedMetadata = true;
300
+ //log updated paths
301
+ // buildMetadata()
302
+ // .then((updatedPaths) => {
303
+ // // if(updatedPaths && updatedPaths.length)
304
+ // // {
305
+ // // console.log(chalk.blueBright('Updated metadata:\n - '+updatedPaths.join('\n - ')));
306
+ // // }
307
+ // })
308
+ // .catch((err) => {
309
+ // console.warn('Could not update metadata: ' + err);
310
+ // });
311
+ }
312
+ });
313
+ this.server.use(webpackDevMiddleware(compiler, {
314
+ serverSideRender: true,
315
+ publicPath: webpackConfig.output.publicPath,
316
+ stats: {
317
+ children: true,
318
+ version: false,
319
+ chunks: false,
320
+ assets: false,
321
+ entrypoints: false,
322
+ modules: false,
323
+ },
324
+ writeToDisk: true,
325
+ }));
326
+ compiler.hooks.afterEmit.tap('store-manifest', (compilation) => {
327
+ try {
328
+ // Read manifest from the filesystem after webpack writes it
329
+ const manifestPath = path.resolve(compilation.options.output.path, 'manifest.json');
330
+ if (fsNative.existsSync(manifestPath)) {
331
+ const manifestContent = fsNative.readFileSync(manifestPath, 'utf-8');
332
+ this.latestManifest = JSON.parse(manifestContent);
333
+ }
334
+ }
335
+ catch (err) {
336
+ console.warn('Failed to parse manifest from webpack emit:', err);
337
+ }
338
+ });
339
+ this.server.use(webpackHotMiddleware(compiler, {
340
+ log: false,
341
+ }));
342
+ }
343
+ // //map URL routes to file paths
344
+ const oneYear = 1000 * 60 * 60 * 24 * 365; // in milliseconds
345
+ const oneMonth = 1000 * 60 * 60 * 24 * 30; // in milliseconds
346
+ this.server.use('/public', express.static('./public', {
347
+ maxAge: oneYear, // Tell browser to cache for 1 year
348
+ immutable: true, // Suggest that the content won't change
349
+ }));
350
+ this.server.use('/uploads', express.static('./data/uploads', {
351
+ maxAge: oneYear, // Tell browser to cache for 1 year
352
+ immutable: true, // Suggest that the content won't change
353
+ }));
354
+ this.server.use('/', express.static('./public/root'));
355
+ this.server.use('/favicon.ico', express.static('./public/favicon.ico', {
356
+ maxAge: oneMonth, // Tell browser to cache for 1 year
357
+ immutable: true, // Suggest that the content won't change
358
+ }));
359
+ this.server.use('/.well-known', express.static('./public/.well-known'));
360
+ // this.server.post('/data',this.handleErrorsJson(async (req,res) => this.serveData(req, res)));
361
+ this.server.get('/resized/*', async (req, res) => {
362
+ this.resizeImage(req, res);
363
+ });
364
+ // Scoped-package variants (@scope/pkg). Register BEFORE the unscoped
365
+ // routes — Express `:pkg` won't consume slashes, so a call to
366
+ // `/call/@_linked/auth/signinDev` would otherwise fall through to the
367
+ // 3-segment `:pkg/:shape/:method` route and be misinterpreted as a
368
+ // shape-method call (pkg=`@_linked`, shape=`auth`). These handlers
369
+ // recognise the `@scope/pkg` prefix and rebuild the full package name
370
+ // before dispatching to the same handler used for unscoped packages.
371
+ this.server.post('/call/@:scope/:pkg/:method', this.handleErrorsJson(async (req, res) => {
372
+ req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
373
+ return this.processBackendMethodCall(req, res);
374
+ }));
375
+ this.server.post('/call/@:scope/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => {
376
+ req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
377
+ return this.processShapeMethodCall(req, res);
378
+ }));
379
+ this.server.post('/call/:pkg/:method', this.handleErrorsJson(async (req, res) => this.processBackendMethodCall(req, res)));
380
+ this.server.post('/call/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => this.processShapeMethodCall(req, res)));
381
+ this.server.post('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'post')));
382
+ this.server.get('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'get')));
383
+ // this.server.post(
384
+ // '/api/query/:method',
385
+ // this.handleErrorsJson(async (req, res) => this.processQuery(req, res)),
386
+ // );
387
+ // now that all the middleware is defined, we initialise the providers, before we define a catch-all route
388
+ // before catch all
389
+ // await this.initBackendProviders();
390
+ await this.callGenericBackendProvidersMethod('setupBeforeCatchAllControllers');
391
+ // HEAD catch-all for maintenance/health check (client fetches SITE_ROOT with method: HEAD)
392
+ this.server.head('__health', (_req, res) => {
393
+ res.sendStatus(200);
394
+ });
395
+ this.server.get('*', this.handleErrors(async (req, res) => {
396
+ //make sure the frontend bundle has finished building
397
+ // await this.waitForWebpack();
398
+ this.render(req, res);
399
+ }));
400
+ // after controller
401
+ await this.callGenericBackendProvidersMethod('setupAfterControllers');
402
+ //remove http(s):// and remove port :[port]
403
+ const HOST = process.env.SITE_ROOT.replace(/https?:\/\//, '').replace(/:\d+$/, '');
404
+ //backlog of 1024 means maximum of 1024 connections in the queue (higher than default)
405
+ this.httpServer = this.server.listen({ port: PORT, backlog: 1024 }, () => {
406
+ console.log(`Up and running at http://localhost:${PORT}`);
407
+ // open(`http://localhost:${PORT}`)
408
+ });
409
+ // Ensure all inactive connections are terminated by the ALB, by setting this a few seconds higher than the ALB idle timeout
410
+ this.httpServer.keepAliveTimeout = 60000;
411
+ // Ensure the headersTimeout is set higher than the keepAliveTimeout due to this nodejs regression bug: https://github.com/nodejs/node/issues/27363
412
+ this.httpServer.headersTimeout = 61000;
413
+ this.httpServer.on('error', function (error) {
414
+ if (error['syscall'] !== 'listen') {
415
+ throw error;
416
+ }
417
+ const isPipe = (portOrPipe) => Number.isNaN(portOrPipe);
418
+ const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;
419
+ switch (error['code']) {
420
+ case 'EACCES':
421
+ console.error(bind + ' requires elevated privileges');
422
+ process.exit(1);
423
+ case 'EADDRINUSE':
424
+ console.error(bind + ' is already in use');
425
+ process.exit(1);
426
+ default:
427
+ throw error;
428
+ }
429
+ });
430
+ return this;
431
+ }
432
+ async initOntologies() { }
433
+ /**
434
+ *
435
+ * @returns
436
+ * @todo Check if default file store is set, if not, set it
437
+ */
438
+ async initStores() {
439
+ return Promise.all(LinkedStorage.getDatasets().map((store) => {
440
+ return store.init ? store.init() : Promise.resolve();
441
+ }));
442
+ }
443
+ // get generic backend providers for handle controller methods
444
+ async callGenericBackendProvidersMethod(method, ...args) {
445
+ for (let genericProvider of this.genericProviders.values()) {
446
+ if (!genericProvider || !genericProvider[method]) {
447
+ continue;
448
+ }
449
+ if (typeof genericProvider[method] == 'function') {
450
+ await Promise.resolve(genericProvider[method](...args));
451
+ }
452
+ }
453
+ }
454
+ /**
455
+ * Removes temporary nodes from memory if they've been seen twice
456
+ * Current interval is once per hour.
457
+ * So after 2 hours temporary nodes are removed.
458
+ */
459
+ initGarbageCollection() {
460
+ // let seenLastTime:NodeSet<NamedNode> = null;
461
+ // setInterval(() => {
462
+ //
463
+ // let tempNodes = new NodeSet<NamedNode>();
464
+ // NamedNode.getAllNamedNodes().forEach(n => {
465
+ // if(n.isTemporaryNode) {
466
+ // if(seenLastTime && seenLastTime.has(n))
467
+ // {
468
+ // n.remove();
469
+ // } else {
470
+ // tempNodes.add(n);
471
+ // }
472
+ // }
473
+ // })
474
+ // seenLastTime = tempNodes;
475
+ // }, 1000 * 10); //every 10 sec
476
+ // // }, 1000 * 60 * 60); //every hour
477
+ }
478
+ async initBackendProviders() {
479
+ //get all local workspace lincd packages, then filter to only those
480
+ //in this app's dependency tree. This avoids loading npm-installed
481
+ //legacy packages that may use an old version of the core.
482
+ const allLocalPackages = getLincdPackages();
483
+ const localPackageMap = new Map(allLocalPackages.map((pkg) => [pkg.packageName, pkg]));
484
+ const relevantPackages = this.filterPackagesByDependencyTree(localPackageMap, this.package);
485
+ for (let [pkgName, pkg] of relevantPackages) {
486
+ await this.indexPackageBackendProviders(pkgName);
487
+ await this.indexLincdPackage(pkgName);
488
+ }
489
+ try {
490
+ await fs
491
+ .readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
492
+ .then(async (contents) => {
493
+ let pkg = JSON.parse(contents);
494
+ await this.indexPackageBackendProviders(pkg.name, false, path.join(process.cwd(), 'src', 'backend.ts'));
495
+ });
496
+ }
497
+ catch (err) {
498
+ console.warn(err);
499
+ }
500
+ }
501
+ /**
502
+ * Filters local workspace packages to only those reachable from the app's
503
+ * dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
504
+ */
505
+ filterPackagesByDependencyTree(allPackages, appPackageJson) {
506
+ const relevantPackages = new Map();
507
+ const packagesToCheck = new Set();
508
+ const processedPackages = new Set();
509
+ // Start with direct dependencies from the app
510
+ if (appPackageJson.dependencies) {
511
+ for (const dep of Object.keys(appPackageJson.dependencies)) {
512
+ if (allPackages.has(dep)) {
513
+ packagesToCheck.add(dep);
514
+ }
515
+ }
516
+ }
517
+ // Recursively follow each package's dependencies
518
+ while (packagesToCheck.size > 0) {
519
+ const packageName = packagesToCheck.values().next().value;
520
+ packagesToCheck.delete(packageName);
521
+ if (processedPackages.has(packageName)) {
522
+ continue;
523
+ }
524
+ processedPackages.add(packageName);
525
+ const packageDetails = allPackages.get(packageName);
526
+ if (packageDetails) {
527
+ relevantPackages.set(packageName, packageDetails);
528
+ const pkg = getPackageJSON(packageDetails.path);
529
+ if (pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) {
530
+ for (const dep of Object.keys(pkg.dependencies)) {
531
+ if (allPackages.has(dep) && !processedPackages.has(dep)) {
532
+ packagesToCheck.add(dep);
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ return relevantPackages;
539
+ }
540
+ async resizeImage(req, res) {
541
+ var _a;
542
+ //check if the w or h query parameters are set
543
+ let width = req.query.w;
544
+ let height = req.query.h;
545
+ //if not
546
+ if (!width && !height) {
547
+ //redirect to the original image
548
+ res.redirect(req.originalUrl.replace('/resized', '/uploads'));
549
+ return;
550
+ }
551
+ let imageFileName = req.query.src; // example: https://cdnurl.com/uploads/1710814765388-935b511c9_cropped.jpeg
552
+ const accessURL = LinkedFileStorage.accessURL;
553
+ if (imageFileName) {
554
+ // TODO: restrict resizing to images that are stored by LinkedFileStorage.fileExists()
555
+ // if (imageFileName.startsWith(accessURL)) {
556
+ // const exists = await LinkedFileStorage.fileExists(
557
+ // imageFileName.replace(`${accessURL}/`, ''),
558
+ // );
559
+ // extract the base name and extension from the imageFileName
560
+ // example: /uploads/resized/935b511c9_cropped.jpeg
561
+ const url = new URL(imageFileName);
562
+ const { name, ext } = path.parse(url.pathname);
563
+ // append the width and height parameters to the base name
564
+ // example: 935b511c9_cropped_w190.jpeg or 935b511c9_cropped_w190h190.jpeg
565
+ const newName = `${name}${width ? '_w' + width : ''}${height ? 'h' + height : ''}`;
566
+ // create a new pathname with dimensions
567
+ // example: /uploads/resized/935b511c9_cropped.jpeg -> /uploads/resized/935b511c9_cropped_w190.jpeg
568
+ const newPathname = path.join(path.dirname(url.pathname), 'resized', `${newName}${ext}`);
569
+ // remove the leading slash from the pathname
570
+ // example: /uploads/resized/935b511c9_cropped_w190.jpeg -> uploads/resized/935b511c9_cropped_w190.jpeg
571
+ const resizedImageFileName = newPathname.startsWith('/')
572
+ ? newPathname.slice(1)
573
+ : newPathname;
574
+ if (this.resizePathsMap.has(resizedImageFileName)) {
575
+ res.redirect(this.resizePathsMap.get(resizedImageFileName));
576
+ return;
577
+ }
578
+ // check if the resized image already exists in the CDN
579
+ const imageExists = await LinkedFileStorage.fileExists(resizedImageFileName);
580
+ // if exists, redirect to the resized image
581
+ if (imageExists) {
582
+ const resizedPathOnCdn = accessURL + '/' + resizedImageFileName;
583
+ //save to cache
584
+ this.resizePathsMap.set(resizedImageFileName, resizedPathOnCdn);
585
+ //redirect this request to the resized image
586
+ res.redirect(resizedPathOnCdn);
587
+ return;
588
+ }
589
+ else {
590
+ console.log(`${process.pid} - ${process.env.PORT}: Resizing image: ${imageFileName} to ${width} x ${height || ''}`);
591
+ //in multicore development, we need to access the site from 127.0.0.1, and so all requests need to go there
592
+ // // if (process.env.NODE_ENV === 'development' && process.env.NUM_WORKER_PROCESSES) {
593
+ // // imageFileName = imageFileName.replace('localhost', '127.0.0.1');
594
+ // // }
595
+ // get the image from imageFileName
596
+ const image = await globalThis
597
+ .fetch(imageFileName)
598
+ .then((res) => res.arrayBuffer())
599
+ .then((arrayBuffer) => Buffer.from(arrayBuffer))
600
+ .catch((err) => {
601
+ console.warn('Could not fetch image from URL: ' + err);
602
+ return null;
603
+ });
604
+ // if image is null, return 404
605
+ if (!image) {
606
+ res.status(404).send({ error: 'Could not fetch image from URL' });
607
+ return;
608
+ }
609
+ // get the format of the image
610
+ let format;
611
+ try {
612
+ format = await sharp(image)
613
+ .metadata()
614
+ .then((meta) => meta.format);
615
+ }
616
+ catch (err) {
617
+ console.warn('Unsupported image format: ' + err);
618
+ res.status(400).send({ error: 'Unsupported image format' });
619
+ return;
620
+ }
621
+ // set the output options based on the format for quality and compression
622
+ let outputOptions;
623
+ switch (format) {
624
+ case 'jpeg':
625
+ outputOptions = { quality: 90 };
626
+ break;
627
+ case 'png':
628
+ outputOptions = { compressionLevel: 9 };
629
+ break;
630
+ case 'webp':
631
+ outputOptions = { quality: 90 };
632
+ break;
633
+ // add more formats here
634
+ default:
635
+ outputOptions = {};
636
+ }
637
+ // resize the image
638
+ const resizedImage = await sharp(image)
639
+ .resize(width ? parseInt(width) : null, height ? parseInt(height) : null)
640
+ .toFormat(format, outputOptions)
641
+ .toBuffer()
642
+ .catch((err) => {
643
+ console.warn('Could not resize image: ' + err);
644
+ return null;
645
+ });
646
+ // if resizedImage is null, return 500
647
+ if (!resizedImage) {
648
+ res.status(500).send({ error: 'Could not resize image' });
649
+ return;
650
+ }
651
+ // upload the resized image to the CDN
652
+ const resizedPathOnCdn = await LinkedFileStorage.saveFile(newPathname, resizedImage);
653
+ //save to cache
654
+ this.resizePathsMap.set(resizedImageFileName, resizedPathOnCdn);
655
+ //redirect this request to the resized image
656
+ res.redirect(resizedPathOnCdn);
657
+ return;
658
+ }
659
+ }
660
+ else {
661
+ imageFileName = (_a = req.originalUrl.split('/resized/')[1]) === null || _a === void 0 ? void 0 : _a.split('?')[0];
662
+ }
663
+ //if this request has not been made (and stored on the HD) before
664
+ let [trueFileName, ...extensions] = imageFileName.split('.');
665
+ let extension = extensions.join('.');
666
+ let resizedImageFileName = trueFileName +
667
+ '_' +
668
+ (width ? 'w' + width : '') +
669
+ (height ? 'h' + height : '') +
670
+ '.' +
671
+ extension;
672
+ let resizedFilePath = path.join(process.cwd(), 'data', 'uploads', 'resized', resizedImageFileName);
673
+ if (!fsNative.existsSync(resizedFilePath)) {
674
+ //ensure the resized folder exists
675
+ // if (!fsNative.existsSync(path.join(process.cwd(), 'data', 'uploads', 'resized'))) {
676
+ // fsNative.mkdirSync(path.join(process.cwd(), 'data', 'uploads', 'resized'), {recursive: true});
677
+ // }
678
+ //then lets resize and store the image:
679
+ // resize the image with sharp and return it
680
+ let originalImagePath = path.join(process.cwd(), 'data', 'uploads', imageFileName);
681
+ if (!fsNative.existsSync(originalImagePath)) {
682
+ console.warn('Could not find original image at ' + originalImagePath);
683
+ return res.status(404).send({ error: 'Could not find original image' });
684
+ }
685
+ try {
686
+ let image = sharp(originalImagePath);
687
+ image.resize(width ? parseInt(width) : null, height ? parseInt(height) : null);
688
+ //write image to disk
689
+ await image
690
+ .toFile(resizedFilePath)
691
+ .then(() => {
692
+ // console.log('resized image written to disk: ' + resizedFilePath);
693
+ })
694
+ .catch((err) => {
695
+ console.warn('Could not write resized image to disk at ' +
696
+ resizedFilePath +
697
+ ': ' +
698
+ err);
699
+ });
700
+ //
701
+ // //get content type from the file extension
702
+ // let contentType;
703
+ // let extension = imageFileName.split('.').pop();
704
+ // switch (extension) {
705
+ // case 'jpg':
706
+ // case 'jpeg':
707
+ // contentType = 'image/jpeg';
708
+ // break;
709
+ // case 'png':
710
+ // contentType = 'image/png';
711
+ // break;
712
+ // case 'gif':
713
+ // contentType = 'image/gif';
714
+ // break;
715
+ // default:
716
+ // contentType = 'image/jpeg';
717
+ // }
718
+ // res.setHeader('Content-Type', contentType);
719
+ // image.pipe(res);
720
+ }
721
+ catch (err) {
722
+ console.warn(err);
723
+ res.status(500).send({ error: 'Could not resize image' });
724
+ }
725
+ }
726
+ //send the resized image
727
+ res.sendFile(resizedFilePath);
728
+ }
729
+ async indexLincdPackage(pkg, warnIfNotFound = false) {
730
+ if (pkg === this.package.name) {
731
+ return;
732
+ }
733
+ try {
734
+ // console.log(`🔍 Loading package: ${pkg}`);
735
+ // console.log(
736
+ // `🔍 Module resolution for ${pkg}:`,
737
+ //@ts-ignore
738
+ // await import.meta.resolve(pkg)
739
+ // );
740
+ await import(pkg);
741
+ // console.log(`✅ Successfully loaded: ${pkg}`);
742
+ }
743
+ catch (e) {
744
+ let providerNotFound = e.code === 'MODULE_NOT_FOUND' &&
745
+ e.message.indexOf(`Cannot find package '${pkg}'`) !== -1;
746
+ if (providerNotFound) {
747
+ // console.warn('Error loading ' + providerPath + ': ' + e.stack);
748
+ if (warnIfNotFound) {
749
+ console.warn(chalk.magenta(`Could not load package ${pkg}`, typeof module !== 'undefined' && typeof exports !== 'undefined'
750
+ ? //@ts-ignore
751
+ ' at ' + (await import.meta.resolve(pkg))
752
+ : ''));
753
+ }
754
+ }
755
+ else {
756
+ console.warn(chalk.red(`Error loading '${pkg}' ${typeof module !== 'undefined' && typeof exports !== 'undefined'
757
+ ? //@ts-ignore
758
+ ' at ' + (await import.meta.resolve(pkg))
759
+ : ''}: ${e.message}\n`), e.stack);
760
+ }
761
+ }
762
+ }
763
+ async indexPackageBackendProviders(pkg, warnIfNotFound = false, backendIndexFilePath) {
764
+ if (!backendIndexFilePath) {
765
+ backendIndexFilePath = `${pkg}/backend`;
766
+ }
767
+ let backendProviderExports;
768
+ let genericBackendProvider;
769
+ let shapeProviders = [];
770
+ await import(backendIndexFilePath)
771
+ .then((backendProviderExports) => {
772
+ //instantiate the exported provider classes and add them to the right place
773
+ Object.keys(backendProviderExports).forEach((key) => {
774
+ let providerClass = backendProviderExports[key];
775
+ //always send an instance of the express server
776
+ //TODO: do not create an instance, just save the class and instantiate it when needed
777
+ let provider = new providerClass(this.server, this);
778
+ if (provider instanceof ShapeProvider) {
779
+ shapeProviders.push(provider);
780
+ if (!Object.getOwnPropertyNames(provider).includes('shape')) {
781
+ console.warn(chalk.red(`${Object.getPrototypeOf(provider).constructor.name} in package ${pkg}
782
+ is not properly linked to a shape. Use public shape = SomeShape.`));
783
+ }
784
+ }
785
+ else {
786
+ if (genericBackendProvider) {
787
+ console.warn(`Package ${pkg} exports two generic backend providers. Only one will work`);
788
+ }
789
+ else {
790
+ genericBackendProvider = provider;
791
+ }
792
+ }
793
+ });
794
+ })
795
+ .catch((e) => {
796
+ const match = e.message.match(/module \'([^\']+)'/);
797
+ //check that the imported /backend path is not found (and only that path, not an import IN that file that is not found, that should still throw an error)
798
+ let providerNotFound = e.code === 'ERR_MODULE_NOT_FOUND' &&
799
+ e.message.indexOf(`Cannot find module`) !== -1 &&
800
+ match &&
801
+ match[1] &&
802
+ match[1].includes('/backend');
803
+ if (providerNotFound) {
804
+ // console.warn('Error loading ' + providerPath + ': ' + e.stack);
805
+ if (warnIfNotFound) {
806
+ console.warn(chalk.magenta(`Could not find backend file of package ${pkg}.
807
+ Check:\n
808
+ - Make sure backend.ts exists and is included in tsconfig.json\n
809
+ - Make sure the package name in src/package.ts matches the package name in package.json`));
810
+ }
811
+ }
812
+ else {
813
+ console.warn(chalk.red(`Could not load backend file of module '${pkg}' from ${process.cwd()}:\n`), e.stack);
814
+ }
815
+ genericBackendProvider = null;
816
+ });
817
+ this.genericProviders.set(pkg, genericBackendProvider);
818
+ this.shapeProviders.set(pkg, shapeProviders);
819
+ return { backendProviderExports, shapeProviders };
820
+ }
821
+ async processBackendMethodCall(request, response) {
822
+ this.noCache(response);
823
+ await this.initRequest(request, response);
824
+ let { pkg, method } = request.params;
825
+ let { args } = JSONParser.parseObject(request.body);
826
+ return this.callBackendMethod(pkg, method, args, request, response).then((result) => {
827
+ //some methods of backend providers may choose to work with request/response directly and will not return anything
828
+ //so only if a result is returned
829
+ if (typeof result !== 'undefined') {
830
+ //do we convert it to JSON and send it to the frontend
831
+ this.sendJson(response, result);
832
+ }
833
+ else {
834
+ //in other cases, we still need to close the request and send an empty response
835
+ if (!response.headersSent) {
836
+ this.sendJson(response, null);
837
+ }
838
+ }
839
+ });
840
+ }
841
+ noCache(response) {
842
+ response.setHeader('Surrogate-Control', 'no-store');
843
+ response.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
844
+ response.setHeader('Expires', '0');
845
+ }
846
+ async processAPICall(request, response, method) {
847
+ this.noCache(response);
848
+ const result = await this.api.process(request, response, method);
849
+ }
850
+ // async processQuery(request, response) {
851
+ //
852
+ // this.api.process(request, response);
853
+ // }
854
+ async processShapeMethodCall(request, response) {
855
+ var _a, _b;
856
+ this.noCache(response);
857
+ // console.log('process shape call');
858
+ // response.on('finish', function() {
859
+ // console.log('shape method call request finish');
860
+ // });
861
+ //
862
+ // response.on('close', function() {
863
+ // console.log('close');
864
+ // });
865
+ //
866
+ // response.on('end', function() {
867
+ // console.log('end');
868
+ // });
869
+ //
870
+ // response.on('header', function() {
871
+ // console.log('header');
872
+ // console.log(response.statusCode);
873
+ // });
874
+ await this.initRequest(request, response);
875
+ let { pkg, shape, method } = request.params;
876
+ let { shapeURI, instanceNode, args } = JSONParser.parseObject(request.body);
877
+ if (!shapeURI) {
878
+ if ((_a = request.query) === null || _a === void 0 ? void 0 : _a.shapeURI) {
879
+ shapeURI = (_b = request.query) === null || _b === void 0 ? void 0 : _b.shapeURI;
880
+ }
881
+ else {
882
+ response.status(500).send({
883
+ error: 'Invalid server call request: ' + request.originalUrl,
884
+ });
885
+ console.warn(chalk.red('Invalid server call request: ' + request.originalUrl));
886
+ return;
887
+ }
888
+ }
889
+ return this.callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response).then((result) => {
890
+ //we return json if something was returned or, if nothing was returned, we still close the request if the method has not accessed response itself already to send things over
891
+ if (typeof result !== 'undefined' || !response.headersSent) {
892
+ this.sendJson(response, result);
893
+ }
894
+ });
895
+ }
896
+ async callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response) {
897
+ var _a;
898
+ //- index module providers if not done yet
899
+ if (!this.shapeProviders.has(pkg)) {
900
+ await this.indexPackageBackendProviders(pkg, true);
901
+ }
902
+ let packageShapeProviders = this.shapeProviders.get(pkg);
903
+ let findProviderForShape = (nodeShapeId) => {
904
+ return packageShapeProviders.find((provider) => {
905
+ var _a, _b;
906
+ //access the static shape (which is a linkedShape() / Shape class)
907
+ //then access the SHACL NodeShape of that shape class, and its id
908
+ return (provider instanceof ShapeProvider &&
909
+ ((_b = (_a = provider.shape) === null || _a === void 0 ? void 0 : _a.shape) === null || _b === void 0 ? void 0 : _b.id) === nodeShapeId);
910
+ });
911
+ };
912
+ try {
913
+ //- find matching provider
914
+ let shapeClass = getShapeClass(shapeURI);
915
+ let shapeProvider = findProviderForShape(shapeClass.shape.id);
916
+ if (!shapeProvider) {
917
+ let superShapeClasses = getSuperShapesClasses(shapeClass);
918
+ for (let superShapeClass of superShapeClasses) {
919
+ let superShapeProvider = findProviderForShape((_a = superShapeClass.shape) === null || _a === void 0 ? void 0 : _a.id);
920
+ if (superShapeProvider) {
921
+ shapeProvider = superShapeProvider;
922
+ break;
923
+ }
924
+ }
925
+ }
926
+ if (shapeProvider) {
927
+ //NOTE: if this is a direct call from backend to backend, we won't know the request & response here because those don't get passed to the Server utility
928
+ //if this is an issue, we need to see how we can get those back
929
+ if (request && response) {
930
+ //give the provider a chance to prepare for this request
931
+ //wrap the call in a promise and wait for it, because providers MAY return a promise
932
+ await Promise.resolve(shapeProvider.initRequest(request, response));
933
+ }
934
+ //see if the shapeProvider implements the called method
935
+ if (shapeProvider[method]) {
936
+ //prepare the first argument
937
+ //we want to convert the instance node into an instance of the shape that this shapeProvider provides for
938
+ //let's find the shape class
939
+ if (shapeProvider.shape) {
940
+ //instance nodes are not always sent. A shape can also call a shape provider from a static method without a shape instance.
941
+ if (instanceNode) {
942
+ let providerShapeClass = shapeProvider.shape;
943
+ let instance = new providerShapeClass({
944
+ id: instanceNode.id,
945
+ });
946
+ //always send instanceNode as first argument to static provider methods
947
+ args.unshift(instance);
948
+ }
949
+ try {
950
+ //call the method with the given arguments
951
+ let result = await Promise.resolve(shapeProvider[method].apply(shapeProvider, args));
952
+ return result;
953
+ }
954
+ catch (e) {
955
+ console.warn(`Error whilst calling ${Object.getPrototypeOf(shapeProvider).constructor.name}.${method}(): `, e);
956
+ }
957
+ }
958
+ else {
959
+ console.warn(`${Object.getPrototypeOf(shapeProvider).constructor.name} does not define its own 'static shape' property. Please connect the provider to a shape.`);
960
+ }
961
+ }
962
+ else {
963
+ return this.sendError(response, 501, `${Object.getPrototypeOf(shapeProvider).constructor.name} does not have a method called ${method}`);
964
+ }
965
+ }
966
+ else {
967
+ return this.sendError(response, 501, "Could not find provider for shape '" + shapeURI + "'");
968
+ }
969
+ }
970
+ catch (err) {
971
+ console.warn(`Error whilst trying to access provider of ${pkg}: `, err);
972
+ }
973
+ return null;
974
+ }
975
+ handleErrors(fn) {
976
+ return async function (req, res, next) {
977
+ try {
978
+ return await fn(req, res);
979
+ }
980
+ catch (x) {
981
+ console.log(x);
982
+ next(x);
983
+ }
984
+ };
985
+ }
986
+ handleErrorsJson(fn) {
987
+ return async (req, res, next) => {
988
+ try {
989
+ return await fn(req, res);
990
+ }
991
+ catch (err) {
992
+ this.sendError(res, 500, 'internal server error' + (isDevelopment ? ': ' + (err === null || err === void 0 ? void 0 : err.stack) : ''), 'internal server error: ' + (err === null || err === void 0 ? void 0 : err.stack));
993
+ }
994
+ };
995
+ }
996
+ sendError(res, statusCode = 500, message, logMessage) {
997
+ res === null || res === void 0 ? void 0 : res.status(statusCode);
998
+ if (message) {
999
+ res === null || res === void 0 ? void 0 : res.send({ error: message });
1000
+ console.warn(chalk.red(logMessage || message));
1001
+ }
1002
+ }
1003
+ async render(req, res) {
1004
+ var _a, _b, _c, _d;
1005
+ res.socket.on('error', (error) => {
1006
+ console.error('Fatal socket error', error);
1007
+ });
1008
+ //when render() is called, the request is always an 'initial page request', and the server which will return HTML (so this is a SSR request)
1009
+ //in that case we send data back to the frontend in a <script> tag.
1010
+ //We initiate that object here
1011
+ if (req['frontendData'] == null) {
1012
+ req['frontendData'] = {};
1013
+ }
1014
+ //if we are caching this page
1015
+ if (((_a = this.config.server) === null || _a === void 0 ? void 0 : _a.cachePaths) &&
1016
+ this.config.server.cachePaths.includes(req.url)) {
1017
+ //if there is a cach for this path, send it
1018
+ if (this.cachedPaths.has(req.url)) {
1019
+ // console.log(req.url + ': Returning cached path');
1020
+ const html = this.cachedPaths.get(req.url);
1021
+ // res.setHeader('Content-Type', 'text/html; charset=utf-8');
1022
+ // res.setHeader('Content-Length', Buffer.byteLength(html, 'utf8').toString());
1023
+ // res.status(200).end(html);
1024
+ res.send(html);
1025
+ return;
1026
+ }
1027
+ }
1028
+ let didError = false;
1029
+ let App = ((_b = this.config.server) === null || _b === void 0 ? void 0 : _b.loadAppComponent)
1030
+ ? await this.config.server.loadAppComponent()
1031
+ : null;
1032
+ await this.initRequest(req, res);
1033
+ let { requestLD, requestObject } = await this.getRequestData(req, res);
1034
+ //on the backend we need to inform the hook of the request-data value
1035
+ //on the frontend it will be read from the HTML
1036
+ // setRequestData(requestObject);
1037
+ // Abandon and switch to client rendering if enough time passes.
1038
+ // Try lowering this to see the client recover.
1039
+ // Abandon and switch to client rendering if enough time passes.
1040
+ // Try lowering this to see the client recover.
1041
+ let stream;
1042
+ let timedout = false;
1043
+ // const timeout = setTimeout(() => {
1044
+ // stream.abort(`${req.url}: ⏱ SSR stream took too long — force abort`);
1045
+ // if (!res.headersSent) {
1046
+ // res.statusCode = 500;
1047
+ // res.end('SSR timed out');
1048
+ // }
1049
+ // timedout = true;
1050
+ // }, 8_000);
1051
+ let manifest = this.latestManifest || this.assets.manifest || {};
1052
+ let preloadScripts = [];
1053
+ let preloadStyles = [];
1054
+ let matchedRouteKey = null;
1055
+ // Load routes config if available and extract preload chunks for the current route
1056
+ if ((_c = this.config.server) === null || _c === void 0 ? void 0 : _c.loadRoutes) {
1057
+ try {
1058
+ const routesModule = await this.config.server.loadRoutes();
1059
+ const ROUTES = routesModule.ROUTES ||
1060
+ ((_d = routesModule.default) === null || _d === void 0 ? void 0 : _d.ROUTES) ||
1061
+ routesModule;
1062
+ // Match the current request path to a route
1063
+ let matchedRoute = null;
1064
+ for (const [key, route] of Object.entries(ROUTES)) {
1065
+ if (!(route === null || route === void 0 ? void 0 : route.path))
1066
+ continue;
1067
+ const pathPattern = route.path
1068
+ .replace(/:\w+\??/g, '([^/]+)')
1069
+ .replace(/\*/g, '.*');
1070
+ const regex = new RegExp('^' + pathPattern + '$');
1071
+ if (regex.test(req.path)) {
1072
+ matchedRoute = route;
1073
+ matchedRouteKey = key;
1074
+ break;
1075
+ }
1076
+ }
1077
+ // If we found a matching route with preloadChunks, resolve them to URLs (both JS and CSS)
1078
+ if ((matchedRoute === null || matchedRoute === void 0 ? void 0 : matchedRoute.preloadChunks) &&
1079
+ Array.isArray(matchedRoute.preloadChunks)) {
1080
+ preloadScripts = matchedRoute.preloadChunks
1081
+ .map((chunkName) => manifest[`${chunkName}.js`] ||
1082
+ manifest[`${chunkName}.bundle.js`] ||
1083
+ manifest[`${chunkName}.mjs`])
1084
+ .filter(Boolean);
1085
+ // Also collect CSS chunks for the same routes
1086
+ preloadStyles = matchedRoute.preloadChunks
1087
+ .map((chunkName) => manifest[`${chunkName}.css`])
1088
+ .filter(Boolean);
1089
+ }
1090
+ }
1091
+ catch (err) {
1092
+ console.warn('Failed to resolve preload chunks:', err);
1093
+ }
1094
+ }
1095
+ // Add matched route key to request object for Html component
1096
+ req['matchedRouteKey'] = matchedRouteKey;
1097
+ stream = renderToPipeableStream(React.createElement(React.StrictMode, null,
1098
+ React.createElement(StaticRouter, { location: req.url },
1099
+ React.createElement(AppContextProvider, { assets: this.assets, requestLD: requestLD, requestObject: requestObject, preloadScripts: preloadScripts, preloadStyles: preloadStyles, expressRequest: req, expressResponse: res },
1100
+ React.createElement(App, null)))), {
1101
+ bootstrapScripts: [
1102
+ this.assets['main.js'], //generated webpack bundle from frontend/src
1103
+ ],
1104
+ onShellReady: function () {
1105
+ var _a;
1106
+ res.statusCode = didError ? 500 : 200;
1107
+ res.setHeader('Content-type', 'text/html');
1108
+ // Create a caching transform stream à la mxstbr.com/thoughts/streaming-ssr
1109
+ if (((_a = this.config.server) === null || _a === void 0 ? void 0 : _a.cachePaths) &&
1110
+ this.config.server.cachePaths.includes(req.url)) {
1111
+ const bufferedChunks = [];
1112
+ const cacheStream = new Transform({
1113
+ transform(chunk, _enc, cb) {
1114
+ bufferedChunks.push(chunk); // keep a copy
1115
+ cb(null, chunk); // forward unchanged
1116
+ },
1117
+ flush: (cb) => {
1118
+ const html = Buffer.concat(bufferedChunks).toString('utf8');
1119
+ this.cachedPaths.set(req.url, html);
1120
+ if (this.config.server.cacheTimeout) {
1121
+ setTimeout(() => this.cachedPaths.delete(req.url), this.config.server.cacheTimeout);
1122
+ }
1123
+ clearTimeout(timeout); // rendering finished → stop timer
1124
+ cb();
1125
+ },
1126
+ });
1127
+ // Pipe the caching stream into the real response
1128
+ cacheStream.pipe(res);
1129
+ // React may **only be piped once**, so pipe it to the cacheStream
1130
+ stream.pipe(cacheStream);
1131
+ }
1132
+ else {
1133
+ stream.pipe(res);
1134
+ }
1135
+ }.bind(this),
1136
+ onShellError(x) {
1137
+ didError = true;
1138
+ console.error(x);
1139
+ },
1140
+ });
1141
+ // Abandon and switch to client rendering if enough time passes.
1142
+ // Try lowering this to see the client recover.
1143
+ const timeout = setTimeout(() => {
1144
+ stream.abort('⏱ SSR stream took too long — force abort');
1145
+ if (!res.headersSent) {
1146
+ res.statusCode = 500;
1147
+ res.end('SSR timed out');
1148
+ }
1149
+ }, 10000);
1150
+ res.on('close', () => {
1151
+ clearTimeout(timeout); // if you're using a safety timeout
1152
+ });
1153
+ }
1154
+ //If we ever need to use multiple webpack entry-points and different bundle names, then uncomment this
1155
+ /*getWebpackAssets()
1156
+ {
1157
+ // const { devMiddleware } = res.locals.webpack;
1158
+ // const outputFileSystem = devMiddleware.outputFileSystem;
1159
+ // const jsonWebpackStats = devMiddleware.stats.toJson();
1160
+ // const { assetsByChunkName, outputPath } = jsonWebpackStats;
1161
+ //
1162
+ // let normalizedAssets = normalizeAssets(assetsByChunkName.main);
1163
+ // let cssAssets = normalizedAssets
1164
+ // .filter((path) => path.endsWith(".css"))
1165
+ // .map((filePath) => outputFileSystem.readFileSync(path.join(outputPath, filePath)))
1166
+ // .join("\n");
1167
+ //
1168
+ // let jsAssets = normalizeAssets(assetsByChunkName.main)
1169
+ // .filter((path) => path.endsWith(".js"))
1170
+ // .map((path) => `<script src="${path}"></script>`)
1171
+ // .join("\n")}
1172
+ }*/
1173
+ sendJson(res, obj) {
1174
+ let jsonObject = JSONWriter.toJsObject(obj);
1175
+ res.json(jsonObject);
1176
+ }
1177
+ async initRequest(request, response) {
1178
+ // initialise the request for all providers, do it synchroniously, one after the other
1179
+ let p = Promise.resolve();
1180
+ [...this.genericProviders.values()]
1181
+ .filter(Boolean)
1182
+ .forEach((backendProvider) => {
1183
+ p = p
1184
+ .then(() => {
1185
+ return backendProvider.initRequest(request, response);
1186
+ })
1187
+ .catch((err) => {
1188
+ console.warn(`Error during initRequest for provider ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1189
+ });
1190
+ });
1191
+ return p;
1192
+ }
1193
+ async getRequestData(request, response) {
1194
+ // Phase 1: providers return plain JSON data via supplyDataForRequest.
1195
+ // requestLD is kept as empty string for now — SSR data seeding will be
1196
+ // reworked in Phase 2/3 to inject query results instead of graph data.
1197
+ let requestData = {};
1198
+ await Promise.all([...this.genericProviders.values()].map((backendProvider) => {
1199
+ if (backendProvider) {
1200
+ return Promise.resolve(backendProvider.supplyDataForRequest(request, response, requestData)).catch((err) => {
1201
+ console.warn(`Error requesting page-request data from ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1202
+ });
1203
+ }
1204
+ }));
1205
+ let requestLD = '';
1206
+ let requestObject = JSONWriter.stringify(request['frontendData']);
1207
+ return { requestLD, requestObject };
1208
+ }
1209
+ async callBackendMethod(pkg, method, args, request, response) {
1210
+ if (!this.genericProviders.has(pkg)) {
1211
+ await this.indexPackageBackendProviders(pkg, true);
1212
+ }
1213
+ //retrieve the indexed provider class and create a new instance for this request
1214
+ let genericBackendProvider = this.genericProviders.get(pkg);
1215
+ let result;
1216
+ if (!genericBackendProvider) {
1217
+ console.warn(`${chalk.magenta(pkg)} does not have a generic backend provider. If you can edit this package, make sure 'backend.ts' is included in 'tsconfig.json' and that it exports a provider.`);
1218
+ return null;
1219
+ }
1220
+ //test if there is a matching method in the backend provider
1221
+ if (!genericBackendProvider[method]) {
1222
+ console.warn(`Generic provider '${Object.getPrototypeOf(genericBackendProvider).constructor.name}' of ${pkg} does not have a method called ${method}`);
1223
+ return null;
1224
+ }
1225
+ try {
1226
+ //TODO: remove init request.
1227
+ //TODO: refactor this.request and this.response to a request parameter
1228
+ //NOTE: if this is a direct call from backend to backend, we won't know the request & response here because those don't get passed to the Server utility
1229
+ //if this is an issue, we need to see how we can get those back
1230
+ if (request && response) {
1231
+ //initialise the request for this specific provider
1232
+ //wrap the call in a promise and wait for it, because providers MAY return a promise
1233
+ await Promise.resolve(genericBackendProvider.initRequest(request, response));
1234
+ }
1235
+ // args.push(request);
1236
+ // args.push(response);
1237
+ //call the method with the given arguments and return the result as json
1238
+ result = await Promise.resolve(genericBackendProvider[method].apply(genericBackendProvider, args));
1239
+ }
1240
+ catch (e) {
1241
+ console.warn(`Error whilst calling ${method}() in provider ${Object.getPrototypeOf(genericBackendProvider).constructor.name} of package ${pkg}:\n`, e);
1242
+ // error logging
1243
+ LinkedErrorLogging.log(e);
1244
+ }
1245
+ return result;
1246
+ }
1247
+ };
1248
+ /**
1249
+ * indicates that instances of this shape need to have this rdf.type
1250
+ */
1251
+ LincdServer.targetClass = lincdServer.LincdServer;
1252
+ LincdServer = __decorate([
1253
+ linkedShape,
1254
+ __metadata("design:paramtypes", [Object])
1255
+ ], LincdServer);
1256
+ export { LincdServer };
1257
+ //# sourceMappingURL=LincdServer.js.map