@_linked/server 2.14.3 → 2.16.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @\_linked/server
2
2
 
3
+ ## 2.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#122](https://github.com/linked-fw/server/pull/122) [`b6c2c9a`](https://github.com/linked-fw/server/commit/b6c2c9ac689c614966f97f39cf18c0bab7683e16) Thanks [@flyon](https://github.com/flyon)! - Load the backend providers of installed linked packages at boot, not only those of local workspace packages.
8
+
9
+ Every package in the app's dependency tree whose package.json declares `"linkedPackage": true` — registry install, localized checkout or workspace member — now has its `/backend` providers indexed at startup, each after the linked packages it depends on. Before, a registry-installed package's provider was indexed only on the first `/call/<pkg>/...`, so its `initRequest` / `supplyDataForRequest` hooks were silent until then and its `setupBeforeControllers` / `setupAfterControllers` never ran at all. With `@_linked/auth` installed from the registry, that meant a signed-in SSR page after a restart shipped an empty request-json and failed to hydrate.
10
+
11
+ The `"linkedPackage": true` flag is the only test, whatever the package's scope: an `@_linked/*` package without it (such as the `@_linked/localize` CLI tool) is not loaded, and a package in any other scope with it is. Legacy `"lincd": true` packages are still not loaded at boot.
12
+
13
+ Lazy indexing on `/call` stays as a fallback and shares one in-flight indexing per package with boot, so a provider is never constructed twice.
14
+
15
+ A provider whose hook throws no longer takes the server down with it. Boot lifecycle hooks (`setupBeforeControllers`, `setupBeforeCatchAllControllers`, `setupAfterControllers`) and per-request hooks (`initRequest`, `supplyDataForRequest`) now run per provider: an error is logged as `[linked] <package> <hook> failed: …` and the remaining providers still run. Before, a throwing boot hook aborted startup, and a synchronous throw in `supplyDataForRequest` failed every page request without naming the provider. This applies to local workspace providers as well as installed ones.
16
+
17
+ In dev, a package's backend is now loaded by the loader that owns the package: `vite.ssrLoadModule` for what the SSR runner bundles (`ssr.noExternal` — workspaces and `SSR_ENTRY_PACKAGES`), Node's `import()` for what it leaves external. Loading an external package's `/backend` through `ssrLoadModule` evaluated that package a second time, apart from the copy the app's own imports use.
18
+
19
+ Apps should be on `@_linked/sentry` 1.1.3 or later: before that, its `setupBeforeControllers` called `require` in ESM and threw `ReferenceError: require is not defined` whenever `SENTRY_DSN` and `SITE_ROOT` are set outside development. Now that that hook runs at boot, an older version logs the error instead of aborting startup, but Sentry stays inactive.
20
+
21
+ ## 2.15.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#117](https://github.com/linked-fw/server/pull/117) [`05d29f1`](https://github.com/linked-fw/server/commit/05d29f11fa634436de4c5c8f9940790a90e3a222) Thanks [@flyon](https://github.com/flyon)! - Boot-time shape sync no longer deletes store shapes the app did not register.
26
+
27
+ `LinkedServer` materializes the app's registered shapes into its app-data on every boot. It called
28
+ core `syncShapes(appData)` with no options, so on the cores where `orphanScope` defaults to `'all'`
29
+ it also deleted every NodeShape in the dataset that the process had not registered. An app whose
30
+ backend imports no shapes — the Create Now app template — therefore deleted every NodeShape in its
31
+ app-data on boot, including shapes Create Now put there with `bindShape` / `enableCapability`; one
32
+ that deep-imported a single shape file deleted the rest of that package.
33
+
34
+ Boot sync now passes `{orphanScope: 'none'}` explicitly, so it is additive whatever core version the
35
+ app resolves. An operator who wants the old pruning can set `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`
36
+ (that exact value; anything else leaves pruning off). Each boot logs one line naming the mode that
37
+ ran. The helpers are exported from `@_linked/server/utils/syncShapes`.
38
+
3
39
  ## 2.14.3
4
40
 
5
41
  ### Patch Changes
@@ -38,6 +38,7 @@ export declare class LinkedServer extends Shape {
38
38
  private analyse;
39
39
  private shapeProviders;
40
40
  private genericProviders;
41
+ private providerIndexing;
41
42
  private resizePathsMap;
42
43
  private api;
43
44
  /**
@@ -54,15 +55,18 @@ export declare class LinkedServer extends Shape {
54
55
  * Materialize this server's registered shapes into the RDF store as SHACL
55
56
  * (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
56
57
  * app's app-data holds the shapes its instances validate/query against
57
- * (plan-010 T1e.2). This is the canonical, uniform reuse+create mechanism:
58
- * whatever shapes the app package registers (published re-exports + generated)
59
- * get materialized on boot (and re-run on shape-file HMR).
58
+ * (plan-010 T1e.2). Whatever shapes the app package registers (published
59
+ * re-exports + generated) get written on boot (and re-run on shape-file HMR).
60
+ *
61
+ * Additive only: store shapes this process did not register are left alone
62
+ * unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
63
+ * `bootOrphanScope` in utils/syncShapes for why.
60
64
  *
61
65
  * Gated to servers whose DEFAULT dataset is a **concrete** materializable store
62
66
  * (detected via `rawQuery`) — i.e. an app pointing at its app-data FusekiStore.
63
- * CN's default is the context-routing `AppDataRouter` (no `rawQuery`; a bare
64
- * `syncShapes` orphan-read would throw with no active project), so CN is skipped
65
- * here — it materializes its own pinned native shapes in its storage config.
67
+ * CN's default is the context-routing `AppDataRouter` (no `rawQuery`), so CN is
68
+ * skipped here — it materializes its own pinned native shapes in its storage
69
+ * config.
66
70
  */
67
71
  private materializeShapesIntoStore;
68
72
  start(): Promise<this>;
@@ -73,6 +77,15 @@ export declare class LinkedServer extends Shape {
73
77
  * @todo Check if default file store is set, if not, set it
74
78
  */
75
79
  initStores(): Promise<any[]>;
80
+ /**
81
+ * Run a lifecycle hook (setupBeforeControllers, setupBeforeCatchAllControllers,
82
+ * setupAfterControllers) on every generic provider, in indexing order.
83
+ *
84
+ * Each provider is isolated: a hook that throws (or rejects) is logged with
85
+ * the package and hook name and the next provider still runs, so one broken
86
+ * provider does not abort boot. That provider's routes or middleware from
87
+ * that hook may be missing, but the rest of the app starts.
88
+ */
76
89
  callGenericBackendProvidersMethod(method: string, ...args: any[]): Promise<void>;
77
90
  /**
78
91
  * Removes temporary nodes from memory if they've been seen twice
@@ -81,6 +94,24 @@ export declare class LinkedServer extends Shape {
81
94
  */
82
95
  initGarbageCollection(): void;
83
96
  initBackendProviders(): Promise<void>;
97
+ /**
98
+ * Index the backend providers of every linked package in the app's
99
+ * dependency tree, dependencies first. Workspace members (`localPackages`)
100
+ * are also imported whole, as before; for anything else only its `/backend`
101
+ * entry is loaded, and a package without one costs a failed resolve.
102
+ */
103
+ private indexLinkedDependencyProviders;
104
+ /**
105
+ * Index a package's backend providers once. Shared by boot and the lazy
106
+ * path in callBackendMethod / callShapeMethod. `onSourceChange` bypasses it
107
+ * on purpose: an HMR reload must re-index.
108
+ */
109
+ ensurePackageBackendProviders(pkg: string, warnIfNotFound?: boolean): Promise<void>;
110
+ /**
111
+ * Node's `import()`, as a method so a test can resolve a fixture app's
112
+ * node_modules (a test runner resolves bare specifiers from this file).
113
+ */
114
+ protected importModule(specifier: string): Promise<any>;
84
115
  /**
85
116
  * Filters local workspace packages to only those reachable from the app's
86
117
  * dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
@@ -36,6 +36,42 @@ import { autoLoadOntologyData } from '@_linked/core/utils/Package';
36
36
  import { getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
37
37
  import path from 'path';
38
38
  import { pathToFileURL } from 'node:url';
39
+ import { discoverLinkedDependencies, isBundledBySsr, ssrNoExternalOf, } from '../utils/linkedDependencies.js';
40
+ /**
41
+ * `instanceof ShapeProvider`, tolerating a second copy of server-utils.
42
+ *
43
+ * A registry-installed package's backend is loaded by Node (see
44
+ * isBundledBySsr), so in a dev runner that BUNDLES server-utils — the
45
+ * standalone case — its providers extend Node's copy of ShapeProvider, not the
46
+ * one this module imported. Fall back to the class name along the prototype
47
+ * chain. Production has one loader and one copy, so instanceof decides there.
48
+ */
49
+ function isShapeProvider(provider) {
50
+ var _a;
51
+ if (provider instanceof ShapeProvider)
52
+ return true;
53
+ for (let proto = provider ? Object.getPrototypeOf(provider) : null; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
54
+ if (((_a = proto.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'ShapeProvider')
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ /**
60
+ * Call one provider's hook and contain its failure: a synchronous throw or a
61
+ * rejection is logged, naming the package and the hook, and swallowed. Used
62
+ * for hooks that run for every provider (boot lifecycle hooks and per-request
63
+ * hooks), where one provider's bug must not take down boot or every request
64
+ * without saying which provider it was.
65
+ */
66
+ async function runProviderHook(pkg, hook, call) {
67
+ var _a;
68
+ try {
69
+ await call();
70
+ }
71
+ catch (err) {
72
+ console.error(chalk.red(`[linked] ${pkg} ${hook} failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`), (err === null || err === void 0 ? void 0 : err.stack) ? `\n${err.stack}` : '');
73
+ }
74
+ }
39
75
  import process from 'process';
40
76
  import * as React from 'react';
41
77
  import { renderToPipeableStream, renderToStaticMarkup } from 'react-dom/server';
@@ -51,6 +87,7 @@ import { resolveBootstrapEntry, resolveViteMainEntry, resolveViteServingMode, }
51
87
  import { resolveRouteAssets } from '../utils/routeAssets.js';
52
88
  import { resolveRouteComponent, ssrCssInlineUrls, ssrCssScope, } from '../utils/ssrCss.js';
53
89
  import { indexShapesIntoMemory } from '../utils/Shapes.js';
90
+ import { materializeShapes } from '../utils/syncShapes.js';
54
91
  import { installSpaFallback, repinSpaFallback, } from '../utils/spaFallback.js';
55
92
  import { LincdAPI } from './LincdAPI.js';
56
93
  //prevent errors in node.js when (s)css files are imported in js
@@ -124,6 +161,10 @@ let LinkedServer = class LinkedServer extends Shape {
124
161
  this.analyse = false;
125
162
  this.shapeProviders = new Map();
126
163
  this.genericProviders = new Map();
164
+ // One indexing per package: boot and the lazy /call path share it, so two
165
+ // concurrent first calls (or a call racing boot) never construct a
166
+ // package's providers twice.
167
+ this.providerIndexing = new Map();
127
168
  //from resizedFileName to full resized path (CDN or similar)
128
169
  this.resizePathsMap = new Map();
129
170
  if (config && typeof config !== 'string' && !('id' in config)) {
@@ -193,15 +234,18 @@ let LinkedServer = class LinkedServer extends Shape {
193
234
  * Materialize this server's registered shapes into the RDF store as SHACL
194
235
  * (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
195
236
  * app's app-data holds the shapes its instances validate/query against
196
- * (plan-010 T1e.2). This is the canonical, uniform reuse+create mechanism:
197
- * whatever shapes the app package registers (published re-exports + generated)
198
- * get materialized on boot (and re-run on shape-file HMR).
237
+ * (plan-010 T1e.2). Whatever shapes the app package registers (published
238
+ * re-exports + generated) get written on boot (and re-run on shape-file HMR).
239
+ *
240
+ * Additive only: store shapes this process did not register are left alone
241
+ * unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
242
+ * `bootOrphanScope` in utils/syncShapes for why.
199
243
  *
200
244
  * Gated to servers whose DEFAULT dataset is a **concrete** materializable store
201
245
  * (detected via `rawQuery`) — i.e. an app pointing at its app-data FusekiStore.
202
- * CN's default is the context-routing `AppDataRouter` (no `rawQuery`; a bare
203
- * `syncShapes` orphan-read would throw with no active project), so CN is skipped
204
- * here — it materializes its own pinned native shapes in its storage config.
246
+ * CN's default is the context-routing `AppDataRouter` (no `rawQuery`), so CN is
247
+ * skipped here — it materializes its own pinned native shapes in its storage
248
+ * config.
205
249
  */
206
250
  async materializeShapesIntoStore() {
207
251
  var _a;
@@ -221,16 +265,8 @@ let LinkedServer = class LinkedServer extends Shape {
221
265
  try {
222
266
  const { syncShapes } = await import('@_linked/core');
223
267
  // Explicit target: materialize EVERY registered shape into the app's own
224
- // data store regardless of per-shape routing/pins — syncShapes(ds) threads
225
- // ds through the orphan-read + every delete→recreate. (No reliance on
226
- // "whatever the default resolves to per shape".)
227
- const thunks = await syncShapes(appData);
228
- // Batched (not all-at-once) so we don't overwhelm Fuseki — the API hands
229
- // back unexecuted thunks precisely so the caller paces them.
230
- for (let i = 0; i < thunks.length; i += 8) {
231
- await Promise.all(thunks.slice(i, i + 8).map((run) => run()));
232
- }
233
- console.log(`[LinkedServer] materialized ${thunks.length} shape(s) into app-data`);
268
+ // data store regardless of per-shape routing/pins.
269
+ await materializeShapes(appData, syncShapes);
234
270
  }
235
271
  catch (err) {
236
272
  console.warn('[LinkedServer] shape materialization failed (non-fatal):', err);
@@ -503,15 +539,21 @@ let LinkedServer = class LinkedServer extends Shape {
503
539
  return store.init ? store.init() : Promise.resolve();
504
540
  }));
505
541
  }
506
- // get generic backend providers for handle controller methods
542
+ /**
543
+ * Run a lifecycle hook (setupBeforeControllers, setupBeforeCatchAllControllers,
544
+ * setupAfterControllers) on every generic provider, in indexing order.
545
+ *
546
+ * Each provider is isolated: a hook that throws (or rejects) is logged with
547
+ * the package and hook name and the next provider still runs, so one broken
548
+ * provider does not abort boot. That provider's routes or middleware from
549
+ * that hook may be missing, but the rest of the app starts.
550
+ */
507
551
  async callGenericBackendProvidersMethod(method, ...args) {
508
- for (let genericProvider of this.genericProviders.values()) {
509
- if (!genericProvider || !genericProvider[method]) {
552
+ for (let [pkg, genericProvider] of this.genericProviders.entries()) {
553
+ if (!genericProvider || typeof genericProvider[method] !== 'function') {
510
554
  continue;
511
555
  }
512
- if (typeof genericProvider[method] == 'function') {
513
- await Promise.resolve(genericProvider[method](...args));
514
- }
556
+ await runProviderHook(pkg, method, () => genericProvider[method](...args));
515
557
  }
516
558
  }
517
559
  /**
@@ -539,16 +581,21 @@ let LinkedServer = class LinkedServer extends Shape {
539
581
  // // }, 1000 * 60 * 60); //every hour
540
582
  }
541
583
  async initBackendProviders() {
542
- //get all local workspace lincd packages, then filter to only those
543
- //in this app's dependency tree. This avoids loading npm-installed
544
- //legacy packages that may use an old version of the core.
584
+ // Local workspace packages in this app's dependency tree. These are also
585
+ // imported whole at boot (indexLincdPackage), not only their /backend.
545
586
  const allLocalPackages = getLincdPackages();
546
587
  const localPackageMap = new Map(allLocalPackages.map((pkg) => [pkg.packageName, pkg]));
547
588
  const relevantPackages = this.filterPackagesByDependencyTree(localPackageMap, this.package);
548
- for (let [pkgName, pkg] of relevantPackages) {
549
- await this.indexPackageBackendProviders(pkgName);
550
- await this.indexLincdPackage(pkgName);
551
- }
589
+ // Every linked package in the dependency tree, installed or local, each
590
+ // after the linked packages it depends on. Registry installs and
591
+ // localized checkouts are not workspace members, and used to be indexed
592
+ // only lazily, on the first /call/<pkg>/... A provider can hook every
593
+ // request (initRequest, supplyDataForRequest, setupBeforeControllers
594
+ // routes), so those hooks stayed silent until some RPC to the package
595
+ // happened — an installed @_linked/auth did not put the session into a
596
+ // page's request data until then, and its setupBeforeControllers
597
+ // middleware never registered at all.
598
+ await this.indexLinkedDependencyProviders(relevantPackages);
552
599
  try {
553
600
  await fs
554
601
  .readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
@@ -573,6 +620,67 @@ let LinkedServer = class LinkedServer extends Shape {
573
620
  console.warn(err);
574
621
  }
575
622
  }
623
+ /**
624
+ * Index the backend providers of every linked package in the app's
625
+ * dependency tree, dependencies first. Workspace members (`localPackages`)
626
+ * are also imported whole, as before; for anything else only its `/backend`
627
+ * entry is loaded, and a package without one costs a failed resolve.
628
+ */
629
+ async indexLinkedDependencyProviders(localPackages) {
630
+ var _a;
631
+ const started = Date.now();
632
+ const installed = [];
633
+ const ordered = discoverLinkedDependencies(process.cwd(), this.package);
634
+ // A workspace member the walk cannot reach through node_modules still
635
+ // loads, after the rest, as it always did.
636
+ const remaining = new Set(localPackages.keys());
637
+ for (const { packageName } of ordered) {
638
+ const isLocal = remaining.delete(packageName);
639
+ await this.ensurePackageBackendProviders(packageName);
640
+ if (isLocal) {
641
+ await this.indexLincdPackage(packageName);
642
+ }
643
+ else if (this.genericProviders.get(packageName) ||
644
+ ((_a = this.shapeProviders.get(packageName)) === null || _a === void 0 ? void 0 : _a.length)) {
645
+ installed.push(packageName);
646
+ }
647
+ }
648
+ for (const packageName of remaining) {
649
+ await this.ensurePackageBackendProviders(packageName);
650
+ await this.indexLincdPackage(packageName);
651
+ }
652
+ if (installed.length > 0) {
653
+ console.log(chalk.gray(`[linked] indexed backend providers of ${ordered.length} linked package(s) in ${Date.now() - started}ms; installed ones with providers: ${installed.join(', ')}`));
654
+ }
655
+ }
656
+ /**
657
+ * Index a package's backend providers once. Shared by boot and the lazy
658
+ * path in callBackendMethod / callShapeMethod. `onSourceChange` bypasses it
659
+ * on purpose: an HMR reload must re-index.
660
+ */
661
+ async ensurePackageBackendProviders(pkg, warnIfNotFound = false) {
662
+ if (this.genericProviders.has(pkg) && this.shapeProviders.has(pkg)) {
663
+ return;
664
+ }
665
+ let indexing = this.providerIndexing.get(pkg);
666
+ if (!indexing) {
667
+ indexing = this.indexPackageBackendProviders(pkg, warnIfNotFound).then(() => undefined);
668
+ this.providerIndexing.set(pkg, indexing);
669
+ }
670
+ try {
671
+ await indexing;
672
+ }
673
+ finally {
674
+ this.providerIndexing.delete(pkg);
675
+ }
676
+ }
677
+ /**
678
+ * Node's `import()`, as a method so a test can resolve a fixture app's
679
+ * node_modules (a test runner resolves bare specifiers from this file).
680
+ */
681
+ importModule(specifier) {
682
+ return import(/* @vite-ignore */ specifier);
683
+ }
576
684
  /**
577
685
  * Filters local workspace packages to only those reachable from the app's
578
686
  * dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
@@ -901,7 +1009,7 @@ let LinkedServer = class LinkedServer extends Shape {
901
1009
  }
902
1010
  }
903
1011
  async indexPackageBackendProviders(pkg, warnIfNotFound = false, backendIndexFilePath) {
904
- var _a;
1012
+ var _a, _b;
905
1013
  if (!backendIndexFilePath) {
906
1014
  backendIndexFilePath = `${pkg}/backend`;
907
1015
  }
@@ -914,9 +1022,19 @@ let LinkedServer = class LinkedServer extends Shape {
914
1022
  // @semantu/create-now/backend), which Node's ESM resolver can't resolve
915
1023
  // from inside an installed package like @_linked/server.
916
1024
  const vite = (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite;
1025
+ // In dev, load a package's backend with the loader that owns the package
1026
+ // (see isBundledBySsr): Vite for the app itself and for what the SSR
1027
+ // runner bundles (workspaces, SSR_ENTRY_PACKAGES), Node for what it
1028
+ // leaves external (registry installs). ssrLoadModule of an EXTERNAL
1029
+ // package's /backend evaluated that package a second time, apart from the
1030
+ // copy the app's own imports reach.
1031
+ const viteOwnsPackage = !!vite &&
1032
+ typeof vite.ssrLoadModule === 'function' &&
1033
+ (pkg === ((_b = this.package) === null || _b === void 0 ? void 0 : _b.name) ||
1034
+ isBundledBySsr(pkg, ssrNoExternalOf(vite)));
917
1035
  const loadModule = async (specifier) => {
918
1036
  var _a, _b, _c;
919
- if (vite && typeof vite.ssrLoadModule === 'function') {
1037
+ if (viteOwnsPackage) {
920
1038
  // For the user app's own backend, resolve to ./src/backend.ts directly.
921
1039
  // Vite reads the app's package.json exports.development condition.
922
1040
  if (specifier === `${this.package.name}/backend`) {
@@ -956,7 +1074,8 @@ let LinkedServer = class LinkedServer extends Shape {
956
1074
  }
957
1075
  return await vite.ssrLoadModule(specifier);
958
1076
  }
959
- // No-vite path (e.g. production runtime).
1077
+ // Node path: production runtime, or a package the dev SSR runner
1078
+ // leaves external.
960
1079
  //
961
1080
  // The app's own backend cannot be imported by name here. A self-reference
962
1081
  // resolves only from inside the package that declares it, and this code
@@ -971,7 +1090,7 @@ let LinkedServer = class LinkedServer extends Shape {
971
1090
  return await import(/* @vite-ignore */ pathToFileURL(target).href);
972
1091
  }
973
1092
  // Fall back to Node's resolver. The dynamic specifier is intentional.
974
- return await import(/* @vite-ignore */ specifier);
1093
+ return await this.importModule(specifier);
975
1094
  };
976
1095
  await loadModule(backendIndexFilePath)
977
1096
  .then((backendProviderExports) => {
@@ -981,7 +1100,7 @@ let LinkedServer = class LinkedServer extends Shape {
981
1100
  //always send an instance of the express server
982
1101
  //TODO: do not create an instance, just save the class and instantiate it when needed
983
1102
  let provider = new providerClass(this.server, this);
984
- if (provider instanceof ShapeProvider) {
1103
+ if (isShapeProvider(provider)) {
985
1104
  shapeProviders.push(provider);
986
1105
  if (!Object.getOwnPropertyNames(provider).includes('shape')) {
987
1106
  console.warn(chalk.red(`${Object.getPrototypeOf(provider).constructor.name} in package ${pkg}
@@ -1196,7 +1315,7 @@ let LinkedServer = class LinkedServer extends Shape {
1196
1315
  var _a;
1197
1316
  //- index module providers if not done yet
1198
1317
  if (!this.shapeProviders.has(pkg)) {
1199
- await this.indexPackageBackendProviders(pkg, true);
1318
+ await this.ensurePackageBackendProviders(pkg, true);
1200
1319
  }
1201
1320
  let packageShapeProviders = this.shapeProviders.get(pkg);
1202
1321
  let findProviderForShape = (nodeShapeId) => {
@@ -1204,7 +1323,7 @@ let LinkedServer = class LinkedServer extends Shape {
1204
1323
  var _a, _b;
1205
1324
  //access the static shape (which is a linkedShape() / Shape class)
1206
1325
  //then access the SHACL NodeShape of that shape class, and its id
1207
- return (provider instanceof ShapeProvider &&
1326
+ return (isShapeProvider(provider) &&
1208
1327
  ((_b = (_a = provider.shape) === null || _a === void 0 ? void 0 : _a.shape) === null || _b === void 0 ? void 0 : _b.id) === nodeShapeId);
1209
1328
  });
1210
1329
  };
@@ -1653,32 +1772,29 @@ let LinkedServer = class LinkedServer extends Shape {
1653
1772
  res.json(jsonObject);
1654
1773
  }
1655
1774
  async initRequest(request, response) {
1656
- // initialise the request for all providers, do it synchroniously, one after the other
1657
- let p = Promise.resolve();
1658
- [...this.genericProviders.values()]
1659
- .filter(Boolean)
1660
- .forEach((backendProvider) => {
1661
- p = p
1662
- .then(() => {
1663
- return backendProvider.initRequest(request, response);
1664
- })
1665
- .catch((err) => {
1666
- console.warn(`Error during initRequest for provider ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1667
- });
1668
- });
1669
- return p;
1775
+ // initialise the request for all providers, one after the other (order
1776
+ // matters: later providers may read what earlier ones put on the request).
1777
+ // A provider that throws is logged by package name and skipped.
1778
+ for (const [pkg, backendProvider] of this.genericProviders.entries()) {
1779
+ if (!backendProvider || typeof backendProvider.initRequest !== 'function') {
1780
+ continue;
1781
+ }
1782
+ await runProviderHook(pkg, 'initRequest', () => backendProvider.initRequest(request, response));
1783
+ }
1670
1784
  }
1671
1785
  async getRequestData(request, response) {
1672
- // Phase 1: providers return plain JSON data via supplyDataForRequest.
1673
- // requestLD is kept as empty string for now — SSR data seeding will be
1674
- // reworked in Phase 2/3 to inject query results instead of graph data.
1786
+ // Providers return plain JSON data via supplyDataForRequest.
1787
+ // requestLD is kept as empty string for now; SSR data seeding will
1788
+ // inject query results instead of graph data.
1789
+ // A provider that throws (synchronously or not) is logged by package
1790
+ // name; the page still renders with the other providers' data.
1675
1791
  let requestData = {};
1676
- await Promise.all([...this.genericProviders.values()].map((backendProvider) => {
1677
- if (backendProvider) {
1678
- return Promise.resolve(backendProvider.supplyDataForRequest(request, response, requestData)).catch((err) => {
1679
- console.warn(`Error requesting page-request data from ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1680
- });
1792
+ await Promise.all([...this.genericProviders.entries()].map(([pkg, backendProvider]) => {
1793
+ if (!backendProvider ||
1794
+ typeof backendProvider.supplyDataForRequest !== 'function') {
1795
+ return;
1681
1796
  }
1797
+ return runProviderHook(pkg, 'supplyDataForRequest', () => backendProvider.supplyDataForRequest(request, response, requestData));
1682
1798
  }));
1683
1799
  let requestLD = '';
1684
1800
  let requestObject = JSONWriter.stringify(request['frontendData']);
@@ -1686,7 +1802,7 @@ let LinkedServer = class LinkedServer extends Shape {
1686
1802
  }
1687
1803
  async callBackendMethod(pkg, method, args, request, response) {
1688
1804
  if (!this.genericProviders.has(pkg)) {
1689
- await this.indexPackageBackendProviders(pkg, true);
1805
+ await this.ensurePackageBackendProviders(pkg, true);
1690
1806
  }
1691
1807
  //retrieve the indexed provider class and create a new instance for this request
1692
1808
  let genericBackendProvider = this.genericProviders.get(pkg);