@_linked/server 3.6.0 → 3.7.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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Settle with `promise`, or reject with `message` after `ms`. The operation is not
3
+ * cancelled; the timeout only stops a caller (or a queue) from waiting on it forever.
4
+ */
5
+ export function withTimeout(promise, ms, message) {
6
+ let timer;
7
+ const timeout = new Promise((_, reject) => {
8
+ timer = setTimeout(() => reject(new Error(message)), ms);
9
+ });
10
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
11
+ }
12
+ /** A positive integer from the environment, else `fallback`. */
13
+ export function envMs(name, fallback, env = process.env) {
14
+ const n = Number(env[name]);
15
+ return Number.isFinite(n) && n > 0 ? n : fallback;
16
+ }
17
+ //# sourceMappingURL=withTimeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTimeout.js","sourceRoot":"","sources":["../../../src/utils/withTimeout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAI,OAAmB,EAAE,EAAU,EAAE,OAAe;IAC7E,IAAI,KAAgD,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC/C,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,KAAK,CAAC,IAAY,EAAE,QAAgB,EAAE,MAA0C,OAAO,CAAC,GAAG;IACzG,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AACpD,CAAC","sourcesContent":["/**\n * Settle with `promise`, or reject with `message` after `ms`. The operation is not\n * cancelled; the timeout only stops a caller (or a queue) from waiting on it forever.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(message)), ms);\n });\n return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));\n}\n\n/** A positive integer from the environment, else `fallback`. */\nexport function envMs(name: string, fallback: number, env: Record<string, string | undefined> = process.env): number {\n const n = Number(env[name]);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n"]}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "3.6.0",
13
+ "version": "3.7.0",
14
14
  "packageManager": "npm@12.2.0",
15
15
  "linkedPackage": true,
16
16
  "type": "module",
@@ -38,9 +38,9 @@
38
38
  "web3"
39
39
  ],
40
40
  "dependencies": {
41
- "@_linked/cli": "^1.45.5",
42
- "@_linked/core": "^2.36.0",
43
- "@_linked/server-utils": "^1.14.0",
41
+ "@_linked/cli": "^1.54.0",
42
+ "@_linked/core": "^2.37.1",
43
+ "@_linked/server-utils": "^1.15.0",
44
44
  "autoprefixer": "^10.4.18",
45
45
  "compression": "^1.7.4",
46
46
  "cors": "^2.8.5",
package/readme.md CHANGED
@@ -26,6 +26,7 @@ If you want to further adjust the functionality of LincdServer yourself, either
26
26
  - [`ping`](#ping)
27
27
  - [Shape catalog per request](#shape-catalog-per-request)
28
28
  - [API-only servers (`server.apiOnly`)](#api-only-servers-serverapionly)
29
+ - [Host control channel (`/admin/*`)](#host-control-channel-admin)
29
30
  - [Provider lifecycle and HMR](#provider-lifecycle-and-hmr)
30
31
  - [When a provider throws, and refusing to start](#when-a-provider-throws-and-refusing-to-start)
31
32
  - [When to implement `dispose()`](#when-to-implement-dispose)
@@ -402,6 +403,34 @@ new LinkedServer({
402
403
 
403
404
  With `apiOnly` the server skips page rendering. It installs no SPA catch-all, so a `GET` for a page that no route handles gets express's plain `404` instead of the app shell. The `/call/...` and `/api/...` routes and the routes that providers register work as usual.
404
405
 
406
+ ## Host control channel (`/admin/*`)
407
+
408
+ A host that starts the app process (Create Now) controls it over HTTP routes on the app's own port. They exist only when the process was started with `CN_APP_ADMIN_SECRET` in its environment; without it they answer 404. Every request carries the secret in `x-cn-admin-secret` (401 otherwise).
409
+
410
+ | Route | Answer |
411
+ |---|---|
412
+ | `GET /admin/health` | 200 `{status: 'ok'}` once the server listens (the boot ontology sync, shape check and shape sync have finished by then) |
413
+ | `POST /admin/restart` | 202, then the process exits; the host starts it again |
414
+ | `POST /admin/sync-shapes` | 200 `{written, orphanScope, reloaded, shapes, ontologies: {written, skipped}, shapeCheck: {errors, warnings}}` (or `{written: 0, skipped, …}` when shape sync is off); 400 `{error}` for a malformed body or a package it may not reload; 500 `{error}` when the sync fails or times out |
415
+
416
+ `POST /admin/sync-shapes` runs the ontology sync and the shape check (see [Ontology sync and the shape check](#ontology-sync-and-the-shape-check)), then re-syncs the registered shapes into the app's data exactly as the boot does (core `syncShapes`: only shapes whose stored `core:shapeHash` differs are written, with the boot's orphan policy and `syncShapesOnBoot` / `LINKED_SYNC_SHAPES_ON_BOOT` switches). The optional body `{packages: [...]}` names packages whose source the host changed; each must be a valid npm package name and the app itself or one of its direct dependencies, never an `@_linked/*` package; the list only says what to reload, it never filters the syncs. In dev their modules are invalidated in Vite and their backends reloaded before the sync, so a shape file written a moment ago is registered; a built app reloads nothing. `reloaded` lists the packages actually reloaded and `shapes` the node shape IRIs registered afterwards, so the host can check that the shape it wrote is there. Syncs run one at a time, as the system; one that takes longer than `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120000) fails and releases the queue. A source reload by itself (HMR) never syncs shapes. A failed ontology sync does not stop the shape sync; the route then answers 500 with the ontology error.
417
+
418
+ Side effects to expect:
419
+
420
+ - **A reload briefly disposes the package's providers.** Reloading a package (on HMR, or for `sync-shapes`) runs `dispose()` on its providers and constructs new ones; a request to that package's routes or methods that arrives in between can fail. Reloads run one at a time; one that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default 60000) rejects and releases the queue.
421
+ - **A restart drops in-flight requests.** `POST /admin/restart` exits the process 50 ms after answering. A host that restarts an app whose server has no `sync-shapes` route (an older `@_linked/server`) to make its boot sync the shapes drops whatever that app was serving.
422
+ - The channel listens on the app's public port, like every other route; only the secret protects it.
423
+
424
+ ### Ontology sync and the shape check
425
+
426
+ Before its shape sync, at boot and on `POST /admin/sync-shapes`, the server:
427
+
428
+ 1. **Registers the installed packages' ontologies** (core `registerInstalledOntologies`): it imports `lib/esm/ontologies/*.register.js` of every linked package it discovered in the app's dependency tree, so a package the app only deep-imports shape files from (`@_linked/schema/shapes/Person`) is synced and checked too, and of every workspace package of the app (`linkedPackage: true` under its `workspaces`), which no package may import (a Create Now clone's `packages/<slug>-ont`). In dev, a package without a built `lib/` registers from `src/ontologies/*.register.ts` through Vite's `ssrLoadModule`; a built app skips it. A register module that fails is logged and the rest go on.
429
+ 2. **Syncs the ontologies** (core `syncOntologies`): the `rdfs:subClassOf` triples of every registered package's ontology data are written into the app's default dataset, so the store's RDFS view infers the class hierarchy. A package whose stored hash matches is skipped, so an unchanged sync writes nothing. `ontologies.written` and `ontologies.skipped` name the packages (a package whose data cannot be read is skipped with core's warning). Sync nodes of packages no longer registered are pruned only with `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, the shape sync's orphan policy.
430
+ 3. **Checks the shapes** against that hierarchy (core `checkShapeHierarchy`, with every package's `dependencies` and `peerDependencies`). A running app is never stopped by it: each finding is logged, and `shapeCheck` counts them by severity.
431
+
432
+ The ontology sync follows the shape sync's switches (`syncShapesOnBoot`, `LINKED_SYNC_SHAPES_ON_BOOT`), at boot and on `POST /admin/sync-shapes` alike: with shape sync off, the route syncs neither ontologies nor shapes (it still registers the packages' ontologies and runs the shape check). `LINKED_SYNC_ONTOLOGIES_ON_BOOT=false` turns off only the boot's ontology sync. The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded at boot either way. A failed ontology sync at boot is logged and the boot goes on.
433
+
405
434
  ## Provider lifecycle and HMR
406
435
 
407
436
  Every provider — generic (`BackendProvider`) or shape-scoped (`ShapeProvider`) — has a lifecycle the framework drives:
@@ -1,47 +0,0 @@
1
- {
2
- "@context": {
3
- "dc": "http://purl.org/dc/elements/1.1/",
4
- "owl": "http://www.w3.org/2002/07/owl#",
5
- "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
6
- "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
7
- "server": "https://linked.cm/ont/server/"
8
- },
9
- "@graph": [
10
- {
11
- "@id": "server:LincdServer",
12
- "@type": "rdfs:Class",
13
- "rdfs:comment": "A HTTP server for LINCD applications, running on node.js.",
14
- "rdfs:isDefinedBy": {
15
- "@id": "server:"
16
- },
17
- "rdfs:label": "LINCD Server"
18
- },
19
- {
20
- "@id": "server:NodeFileStore",
21
- "@type": "rdfs:Class",
22
- "rdfs:comment": "Stores quads in a file in JSONLD format. Made for node.js environments.",
23
- "rdfs:isDefinedBy": {
24
- "@id": "server:"
25
- },
26
- "rdfs:label": "Node file store"
27
- },
28
- {
29
- "@id": "server:BackendFileStore",
30
- "@type": "rdfs:Class",
31
- "rdfs:comment": "A simple quad store for frontends that are served by a LINCD Nodejs Server. Quads get loaded and updated through API calls to the backend. All quads are loaded into local memory in the frontend upon initiation of the app.",
32
- "rdfs:isDefinedBy": {
33
- "@id": "server:"
34
- },
35
- "rdfs:label": "Backend file store"
36
- },
37
- {
38
- "@id": "server:BackendStore",
39
- "@type": "rdfs:Class",
40
- "rdfs:comment": "A base class for backend quad stores. These are quad stores for frontends that are served by a LINCD Nodejs Server. Quads get loaded and updated through API calls to the backend. All quads are loaded into local memory in the frontend upon initiation of the app.",
41
- "rdfs:isDefinedBy": {
42
- "@id": "server:"
43
- },
44
- "rdfs:label": "Backend store"
45
- }
46
- ]
47
- }