@_linked/server 3.5.0 → 3.6.1
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 +12 -0
- package/lib/esm/shapes/LinkedServer.d.ts +61 -19
- package/lib/esm/shapes/LinkedServer.js +131 -71
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +44 -0
- package/lib/esm/utils/adminChannel.js +116 -0
- package/lib/esm/utils/adminChannel.js.map +1 -0
- package/lib/esm/utils/reloadSource.d.ts +12 -0
- package/lib/esm/utils/reloadSource.js +32 -0
- package/lib/esm/utils/reloadSource.js.map +1 -0
- package/lib/esm/utils/withTimeout.d.ts +7 -0
- package/lib/esm/utils/withTimeout.js +17 -0
- package/lib/esm/utils/withTimeout.js.map +1 -0
- package/package.json +1 -1
- package/readme.md +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 3.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#200](https://github.com/linked-fw/server/pull/200) [`935d0ca`](https://github.com/linked-fw/server/commit/935d0caf014cae8c71ae3b40963305ac8d43792c) Thanks [@flyon](https://github.com/flyon)! - `POST /admin/sync-shapes` now answers which packages it reloaded (`reloaded`) and the node shapes registered afterwards (`shapes`), refuses (400) package names that are not valid npm names, `@_linked/*` packages, or neither the app nor one of its direct dependencies, and times out a hung sync (`LINKED_SHAPE_SYNC_TIMEOUT_MS`, default 120 s) or source reload (`LINKED_SOURCE_RELOAD_TIMEOUT_MS`, default 60 s) so the queue is released. The README documents the control channel and the side effects of a reload and a restart.
|
|
8
|
+
|
|
9
|
+
## 3.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#198](https://github.com/linked-fw/server/pull/198) [`8ff798a`](https://github.com/linked-fw/server/commit/8ff798a59104a1283c5a5ec9312a29e56f103c03) Thanks [@flyon](https://github.com/flyon)! - Add `POST /admin/sync-shapes` to the host control channel (`CN_APP_ADMIN_SECRET`). It re-syncs the app's registered shapes into its app data exactly as the boot does (core `syncShapes`, writing only shapes whose stored hash differs) and answers `{written, orphanScope}`; a failed sync answers 500 `{error}`. The optional body `{packages: [...]}` names packages whose source changed on disk: in dev their modules are invalidated in Vite and their backends reloaded first, so a shape file the host has just written is registered before the sync. Source reloads (`onSourceChange`) now run one at a time, and `LinkedServer.resyncShapes()` is available to code in the process.
|
|
14
|
+
|
|
3
15
|
## 3.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -15,6 +15,7 @@ import { Shape } from '@_linked/core/shapes/Shape';
|
|
|
15
15
|
* needs; a plain function, an arrow function or a non-function value is not.
|
|
16
16
|
*/
|
|
17
17
|
export declare function isProviderClass(value: unknown): boolean;
|
|
18
|
+
import { type ShapeSyncResult } from '../utils/adminChannel.js';
|
|
18
19
|
/**
|
|
19
20
|
* Serve the local upload folder at `/uploads` (where `LocalFileStore` files are
|
|
20
21
|
* reachable), with the headers for untrusted files.
|
|
@@ -104,21 +105,56 @@ export declare class LinkedServer extends Shape {
|
|
|
104
105
|
/**
|
|
105
106
|
* Materialize this server's registered shapes into the RDF store as SHACL
|
|
106
107
|
* (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
|
|
107
|
-
* app's app-data holds the shapes its instances validate/query against
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* app's app-data holds the shapes its instances validate/query against.
|
|
109
|
+
* Whatever shapes the app package registers (published re-exports +
|
|
110
|
+
* generated) are written on boot, and again when the host asks for it over
|
|
111
|
+
* the control channel ({@link resyncShapes}). A source reload (HMR) does not
|
|
112
|
+
* sync shapes by itself.
|
|
113
|
+
*
|
|
114
|
+
* Core writes only the shapes whose stored hash differs, so a sync of
|
|
115
|
+
* unchanged shapes writes nothing.
|
|
110
116
|
*
|
|
111
117
|
* Additive only: store shapes this process did not register are left alone
|
|
112
118
|
* unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
|
|
113
119
|
* `bootOrphanScope` in utils/syncShapes for why.
|
|
114
120
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
121
|
+
* Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
|
|
122
|
+
* LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
|
|
123
|
+
* `syncShapesOnBoot: false` in its own linked.config — its default dataset is
|
|
124
|
+
* a context-router that can't materialize context-free, and it syncs its own
|
|
125
|
+
* pinned shapes separately (linked.backend.storage.js).
|
|
120
126
|
*/
|
|
121
127
|
private materializeShapesIntoStore;
|
|
128
|
+
/** One shape sync into the default dataset; throws when it fails. */
|
|
129
|
+
private syncRegisteredShapes;
|
|
130
|
+
/** Tail of the {@link resyncShapes} queue: one re-sync at a time. */
|
|
131
|
+
private shapeSyncQueue;
|
|
132
|
+
/**
|
|
133
|
+
* Re-sync the registered shapes into app data, as the boot does, and report
|
|
134
|
+
* what was written. The host calls it (`POST /admin/sync-shapes`) after it
|
|
135
|
+
* changed the app's shape code, so the app — the only process that loads that
|
|
136
|
+
* code — writes its own shapes and their hashes.
|
|
137
|
+
*
|
|
138
|
+
* `packages` names the packages whose source changed: the app itself or its
|
|
139
|
+
* direct dependencies, never `@_linked/*` ({@link assertReloadablePackages}). In
|
|
140
|
+
* dev (Vite) their modules are invalidated and their backends reloaded first
|
|
141
|
+
* ({@link onSourceChange}), so a shape added since the boot is registered before
|
|
142
|
+
* the sync; without Vite (a built app) nothing is reloaded. The answer names the
|
|
143
|
+
* packages reloaded and the node shapes registered after it, so the host can check
|
|
144
|
+
* that the shape it wrote is there. Runs as the system, never as the caller of a
|
|
145
|
+
* request, one at a time; a run that takes longer than
|
|
146
|
+
* `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120 s) fails and releases the queue.
|
|
147
|
+
*/
|
|
148
|
+
resyncShapes({ packages }?: {
|
|
149
|
+
packages?: string[];
|
|
150
|
+
}): Promise<ShapeSyncResult>;
|
|
151
|
+
/** The packages a host may ask to reload: the app itself and its direct dependencies. */
|
|
152
|
+
private reloadablePackages;
|
|
153
|
+
/**
|
|
154
|
+
* Re-read one package's source from disk (dev only); see {@link resyncShapes}.
|
|
155
|
+
* Resolves whether it reloaded anything.
|
|
156
|
+
*/
|
|
157
|
+
private reloadPackageFromDisk;
|
|
122
158
|
/**
|
|
123
159
|
* Boot the server and listen. Rejects — and makes the process exit non-zero —
|
|
124
160
|
* when a provider throws a fatal error while booting (see utils/fatalError).
|
|
@@ -214,24 +250,30 @@ export declare class LinkedServer extends Shape {
|
|
|
214
250
|
shapeProviders: any[];
|
|
215
251
|
}>;
|
|
216
252
|
/**
|
|
217
|
-
*
|
|
253
|
+
* Install the client-shell catch-all as the last ordinary layer on the
|
|
254
|
+
* router stack. See utils/spaFallback.ts for why ordering has to be
|
|
255
|
+
* re-asserted rather than assumed.
|
|
256
|
+
*/
|
|
257
|
+
private installSpaFallback;
|
|
258
|
+
/** Tail of the {@link onSourceChange} queue: reloads never interleave. */
|
|
259
|
+
private sourceReloadQueue;
|
|
260
|
+
/**
|
|
261
|
+
* Reload one package's backend providers from source.
|
|
218
262
|
*
|
|
219
263
|
* Called by the CLI orchestrator's Vite watcher whenever a `.ts`/`.tsx`
|
|
220
|
-
* file inside a workspace package changes
|
|
221
|
-
* existing providers (so routes, listeners, timers
|
|
222
|
-
* drops them from the registry, and re-runs
|
|
223
|
-
* to pick up the freshly-imported module.
|
|
264
|
+
* file inside a workspace package changes, and by {@link resyncShapes}.
|
|
265
|
+
* Disposes the package's existing providers (so routes, listeners, timers
|
|
266
|
+
* don't accumulate), drops them from the registry, and re-runs
|
|
267
|
+
* indexPackageBackendProviders to pick up the freshly-imported module.
|
|
268
|
+
* Reloads run one at a time: each re-registers routes on the shared express
|
|
269
|
+
* stack. One that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default
|
|
270
|
+
* 60 s) rejects and releases the queue.
|
|
224
271
|
*
|
|
225
272
|
* Dispose calls have a 5 s soft timeout. A hanging dispose logs a
|
|
226
273
|
* warning and is abandoned so HMR doesn't stall the whole dev loop.
|
|
227
274
|
*/
|
|
228
|
-
/**
|
|
229
|
-
* Install the client-shell catch-all as the last ordinary layer on the
|
|
230
|
-
* router stack. See utils/spaFallback.ts for why ordering has to be
|
|
231
|
-
* re-asserted rather than assumed.
|
|
232
|
-
*/
|
|
233
|
-
private installSpaFallback;
|
|
234
275
|
onSourceChange(pkg: string): Promise<void>;
|
|
276
|
+
private reloadPackageSource;
|
|
235
277
|
processBackendMethodCall(request: any, response: any): Promise<any>;
|
|
236
278
|
noCache(response: any): void;
|
|
237
279
|
processAPICall(request: any, response: any, method: 'get' | 'post' | 'put' | 'delete'): Promise<void>;
|
|
@@ -8,7 +8,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
-
import { timingSafeEqual } from 'crypto';
|
|
12
11
|
import events from 'events';
|
|
13
12
|
import express from 'express';
|
|
14
13
|
import fetchCookie from 'fetch-cookie';
|
|
@@ -38,7 +37,7 @@ import { nativeImport } from '../utils/nativeImport.js';
|
|
|
38
37
|
import { createJsonBodyParser } from '../utils/jsonBodyParser.js';
|
|
39
38
|
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
40
39
|
import { autoLoadOntologyData } from '@_linked/core/utils/Package';
|
|
41
|
-
import { getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
|
|
40
|
+
import { getAllShapeClasses, getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
|
|
42
41
|
import path from 'path';
|
|
43
42
|
import { pathToFileURL } from 'node:url';
|
|
44
43
|
import { isFatalError } from '../utils/fatalError.js';
|
|
@@ -140,6 +139,9 @@ import { resolveBootstrapEntry, resolveViteMainEntry, resolveViteServingMode, }
|
|
|
140
139
|
import { resolveRouteAssets } from '../utils/routeAssets.js';
|
|
141
140
|
import { resolveRouteComponent, ssrCssInlineUrls, ssrCssScope, } from '../utils/ssrCss.js';
|
|
142
141
|
import { materializeShapes } from '../utils/syncShapes.js';
|
|
142
|
+
import { AdminRequestError, assertReloadablePackages, registerAdminRoutes, } from '../utils/adminChannel.js';
|
|
143
|
+
import { envMs, withTimeout } from '../utils/withTimeout.js';
|
|
144
|
+
import { invalidateSourceUnder } from '../utils/reloadSource.js';
|
|
143
145
|
import { installSpaFallback, repinSpaFallback, } from '../utils/spaFallback.js';
|
|
144
146
|
import { LincdAPI } from './LincdAPI.js';
|
|
145
147
|
import { getRpcExposureMode, isGenericRpcDisabled, logSafe, resolveCallable, setRpcExposureMode, warnUndeclaredCall, } from '../utils/rpcExposure.js';
|
|
@@ -292,6 +294,10 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
292
294
|
* error is logged and contained.
|
|
293
295
|
*/
|
|
294
296
|
this.booting = false;
|
|
297
|
+
/** Tail of the {@link resyncShapes} queue: one re-sync at a time. */
|
|
298
|
+
this.shapeSyncQueue = Promise.resolve();
|
|
299
|
+
/** Tail of the {@link onSourceChange} queue: reloads never interleave. */
|
|
300
|
+
this.sourceReloadQueue = Promise.resolve();
|
|
295
301
|
if (config && typeof config !== 'string' && !('id' in config)) {
|
|
296
302
|
// The absence of `id` is the runtime discriminator for a LinkedConfig;
|
|
297
303
|
// the widened `{id?: string}` member of the union keeps TS from narrowing
|
|
@@ -360,44 +366,119 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
360
366
|
/**
|
|
361
367
|
* Materialize this server's registered shapes into the RDF store as SHACL
|
|
362
368
|
* (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
|
|
363
|
-
* app's app-data holds the shapes its instances validate/query against
|
|
364
|
-
*
|
|
365
|
-
*
|
|
369
|
+
* app's app-data holds the shapes its instances validate/query against.
|
|
370
|
+
* Whatever shapes the app package registers (published re-exports +
|
|
371
|
+
* generated) are written on boot, and again when the host asks for it over
|
|
372
|
+
* the control channel ({@link resyncShapes}). A source reload (HMR) does not
|
|
373
|
+
* sync shapes by itself.
|
|
374
|
+
*
|
|
375
|
+
* Core writes only the shapes whose stored hash differs, so a sync of
|
|
376
|
+
* unchanged shapes writes nothing.
|
|
366
377
|
*
|
|
367
378
|
* Additive only: store shapes this process did not register are left alone
|
|
368
379
|
* unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
|
|
369
380
|
* `bootOrphanScope` in utils/syncShapes for why.
|
|
370
381
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
382
|
+
* Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
|
|
383
|
+
* LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
|
|
384
|
+
* `syncShapesOnBoot: false` in its own linked.config — its default dataset is
|
|
385
|
+
* a context-router that can't materialize context-free, and it syncs its own
|
|
386
|
+
* pinned shapes separately (linked.backend.storage.js).
|
|
376
387
|
*/
|
|
377
388
|
async materializeShapesIntoStore() {
|
|
389
|
+
try {
|
|
390
|
+
await this.syncRegisteredShapes();
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
console.warn('[LinkedServer] shape materialization failed (non-fatal):', err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** One shape sync into the default dataset; throws when it fails. */
|
|
397
|
+
async syncRegisteredShapes() {
|
|
378
398
|
var _a;
|
|
379
|
-
// Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
|
|
380
|
-
// LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
|
|
381
|
-
// `syncShapesOnBoot: false` in its own linked.config — its default dataset is
|
|
382
|
-
// a context-router that can't materialize context-free, and it syncs its own
|
|
383
|
-
// pinned shapes separately (linked.backend.storage.js).
|
|
384
399
|
const configFlag = (_a = this.config) === null || _a === void 0 ? void 0 : _a.syncShapesOnBoot;
|
|
385
400
|
const envFlag = process.env.LINKED_SYNC_SHAPES_ON_BOOT;
|
|
386
401
|
if (configFlag === false || envFlag === 'false') {
|
|
387
|
-
return;
|
|
402
|
+
return { written: 0, skipped: 'disabled' };
|
|
388
403
|
}
|
|
389
404
|
const appData = LinkedStorage.getDefaultDataset();
|
|
390
405
|
if (!appData)
|
|
391
|
-
return;
|
|
406
|
+
return { written: 0, skipped: 'no-default-dataset' };
|
|
407
|
+
const { syncShapes } = await import('@_linked/core');
|
|
408
|
+
// Explicit target: materialize EVERY registered shape into the app's own
|
|
409
|
+
// data store regardless of per-shape routing/pins.
|
|
410
|
+
const { count, orphanScope } = await materializeShapes(appData, syncShapes);
|
|
411
|
+
return { written: count, orphanScope };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Re-sync the registered shapes into app data, as the boot does, and report
|
|
415
|
+
* what was written. The host calls it (`POST /admin/sync-shapes`) after it
|
|
416
|
+
* changed the app's shape code, so the app — the only process that loads that
|
|
417
|
+
* code — writes its own shapes and their hashes.
|
|
418
|
+
*
|
|
419
|
+
* `packages` names the packages whose source changed: the app itself or its
|
|
420
|
+
* direct dependencies, never `@_linked/*` ({@link assertReloadablePackages}). In
|
|
421
|
+
* dev (Vite) their modules are invalidated and their backends reloaded first
|
|
422
|
+
* ({@link onSourceChange}), so a shape added since the boot is registered before
|
|
423
|
+
* the sync; without Vite (a built app) nothing is reloaded. The answer names the
|
|
424
|
+
* packages reloaded and the node shapes registered after it, so the host can check
|
|
425
|
+
* that the shape it wrote is there. Runs as the system, never as the caller of a
|
|
426
|
+
* request, one at a time; a run that takes longer than
|
|
427
|
+
* `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120 s) fails and releases the queue.
|
|
428
|
+
*/
|
|
429
|
+
async resyncShapes({ packages = [] } = {}) {
|
|
430
|
+
assertReloadablePackages(packages, this.reloadablePackages());
|
|
431
|
+
const timeoutMs = envMs('LINKED_SHAPE_SYNC_TIMEOUT_MS', 120000);
|
|
432
|
+
const run = this.shapeSyncQueue.then(() => withTimeout(runAsSystem(async () => {
|
|
433
|
+
const reloaded = [];
|
|
434
|
+
for (const pkg of packages) {
|
|
435
|
+
if (await this.reloadPackageFromDisk(pkg))
|
|
436
|
+
reloaded.push(pkg);
|
|
437
|
+
}
|
|
438
|
+
const result = await this.syncRegisteredShapes();
|
|
439
|
+
return { ...result, reloaded, shapes: [...getAllShapeClasses().keys()].sort() };
|
|
440
|
+
}, 'shape sync requested by the host'), timeoutMs, `the shape sync did not finish within ${timeoutMs} ms`));
|
|
441
|
+
this.shapeSyncQueue = run.catch(() => undefined);
|
|
442
|
+
return run;
|
|
443
|
+
}
|
|
444
|
+
/** The packages a host may ask to reload: the app itself and its direct dependencies. */
|
|
445
|
+
reloadablePackages() {
|
|
446
|
+
var _a, _b;
|
|
447
|
+
const names = new Set();
|
|
448
|
+
if ((_a = this.package) === null || _a === void 0 ? void 0 : _a.name)
|
|
449
|
+
names.add(this.package.name);
|
|
392
450
|
try {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
451
|
+
const json = JSON.parse(fsNative.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
452
|
+
if (json.name)
|
|
453
|
+
names.add(json.name);
|
|
454
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
455
|
+
for (const name of Object.keys((_b = json[field]) !== null && _b !== void 0 ? _b : {}))
|
|
456
|
+
names.add(name);
|
|
457
|
+
}
|
|
397
458
|
}
|
|
398
|
-
catch (
|
|
399
|
-
|
|
459
|
+
catch (_c) { }
|
|
460
|
+
return names;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Re-read one package's source from disk (dev only); see {@link resyncShapes}.
|
|
464
|
+
* Resolves whether it reloaded anything.
|
|
465
|
+
*/
|
|
466
|
+
async reloadPackageFromDisk(pkg) {
|
|
467
|
+
var _a, _b, _c;
|
|
468
|
+
const root = pkg === ((_a = this.package) === null || _a === void 0 ? void 0 : _a.name) ? process.cwd() : this.packageDirFor(pkg);
|
|
469
|
+
if (!root)
|
|
470
|
+
throw new AdminRequestError(`unknown package: ${pkg}`);
|
|
471
|
+
const graph = (_c = (_b = this.config.server) === null || _b === void 0 ? void 0 : _b.vite) === null || _c === void 0 ? void 0 : _c.moduleGraph;
|
|
472
|
+
if (!graph)
|
|
473
|
+
return false; // a built app: there is no source to reload
|
|
474
|
+
const roots = [root];
|
|
475
|
+
try {
|
|
476
|
+
roots.push(fsNative.realpathSync(root));
|
|
400
477
|
}
|
|
478
|
+
catch (_d) { }
|
|
479
|
+
invalidateSourceUnder(graph, roots);
|
|
480
|
+
await this.onSourceChange(pkg);
|
|
481
|
+
return true;
|
|
401
482
|
}
|
|
402
483
|
// async serveData(req,res) {
|
|
403
484
|
// let nodeURI = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
@@ -608,42 +689,12 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
608
689
|
this.server.head('__health', (_req, res) => {
|
|
609
690
|
res.sendStatus(200);
|
|
610
691
|
});
|
|
611
|
-
//
|
|
612
|
-
// process with
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
// response. timingSafeEqual requires equal-length buffers, so guard length.
|
|
618
|
-
const secretMatches = (provided) => {
|
|
619
|
-
if (!adminSecret || !provided)
|
|
620
|
-
return false;
|
|
621
|
-
const a = Buffer.from(provided);
|
|
622
|
-
const b = Buffer.from(adminSecret);
|
|
623
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
624
|
-
};
|
|
625
|
-
const requireAdmin = (req, res) => {
|
|
626
|
-
if (!adminSecret) {
|
|
627
|
-
res.sendStatus(404); // no control channel in this mode
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
if (!secretMatches(req.get('x-cn-admin-secret'))) {
|
|
631
|
-
res.sendStatus(401);
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
return true;
|
|
635
|
-
};
|
|
636
|
-
this.server.get('/admin/health', (req, res) => {
|
|
637
|
-
if (!requireAdmin(req, res))
|
|
638
|
-
return;
|
|
639
|
-
res.status(200).json({ status: 'ok' });
|
|
640
|
-
});
|
|
641
|
-
this.server.post('/admin/restart', (req, res) => {
|
|
642
|
-
if (!requireAdmin(req, res))
|
|
643
|
-
return;
|
|
644
|
-
res.sendStatus(202);
|
|
645
|
-
// CN's provisioner respawns on exit (see ChildProcessProvisioner).
|
|
646
|
-
setTimeout(() => process.exit(0), 50);
|
|
692
|
+
// The host's control channel (/admin/*), open only when the host started
|
|
693
|
+
// this process with CN_APP_ADMIN_SECRET; see utils/adminChannel.
|
|
694
|
+
registerAdminRoutes(this.server, process.env.CN_APP_ADMIN_SECRET, {
|
|
695
|
+
// The host's provisioner starts the app again after it exits.
|
|
696
|
+
exit: () => process.exit(0),
|
|
697
|
+
syncShapes: (request) => this.resyncShapes(request),
|
|
647
698
|
});
|
|
648
699
|
// after controller
|
|
649
700
|
await this.callGenericBackendProvidersMethod('setupAfterControllers');
|
|
@@ -1377,18 +1428,6 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1377
1428
|
this.shapeProviders.set(pkg, shapeProviders);
|
|
1378
1429
|
return { backendProviderExports, shapeProviders };
|
|
1379
1430
|
}
|
|
1380
|
-
/**
|
|
1381
|
-
* Plan-011 phase 2 — HMR re-index entry point.
|
|
1382
|
-
*
|
|
1383
|
-
* Called by the CLI orchestrator's Vite watcher whenever a `.ts`/`.tsx`
|
|
1384
|
-
* file inside a workspace package changes. Disposes the package's
|
|
1385
|
-
* existing providers (so routes, listeners, timers don't accumulate),
|
|
1386
|
-
* drops them from the registry, and re-runs indexPackageBackendProviders
|
|
1387
|
-
* to pick up the freshly-imported module.
|
|
1388
|
-
*
|
|
1389
|
-
* Dispose calls have a 5 s soft timeout. A hanging dispose logs a
|
|
1390
|
-
* warning and is abandoned so HMR doesn't stall the whole dev loop.
|
|
1391
|
-
*/
|
|
1392
1431
|
/**
|
|
1393
1432
|
* Install the client-shell catch-all as the last ordinary layer on the
|
|
1394
1433
|
* router stack. See utils/spaFallback.ts for why ordering has to be
|
|
@@ -1401,7 +1440,28 @@ let LinkedServer = class LinkedServer extends Shape {
|
|
|
1401
1440
|
this.render(req, res);
|
|
1402
1441
|
}));
|
|
1403
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Reload one package's backend providers from source.
|
|
1445
|
+
*
|
|
1446
|
+
* Called by the CLI orchestrator's Vite watcher whenever a `.ts`/`.tsx`
|
|
1447
|
+
* file inside a workspace package changes, and by {@link resyncShapes}.
|
|
1448
|
+
* Disposes the package's existing providers (so routes, listeners, timers
|
|
1449
|
+
* don't accumulate), drops them from the registry, and re-runs
|
|
1450
|
+
* indexPackageBackendProviders to pick up the freshly-imported module.
|
|
1451
|
+
* Reloads run one at a time: each re-registers routes on the shared express
|
|
1452
|
+
* stack. One that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default
|
|
1453
|
+
* 60 s) rejects and releases the queue.
|
|
1454
|
+
*
|
|
1455
|
+
* Dispose calls have a 5 s soft timeout. A hanging dispose logs a
|
|
1456
|
+
* warning and is abandoned so HMR doesn't stall the whole dev loop.
|
|
1457
|
+
*/
|
|
1404
1458
|
async onSourceChange(pkg) {
|
|
1459
|
+
const timeoutMs = envMs('LINKED_SOURCE_RELOAD_TIMEOUT_MS', 60000);
|
|
1460
|
+
const run = this.sourceReloadQueue.then(() => withTimeout(this.reloadPackageSource(pkg), timeoutMs, `the reload of ${pkg} did not finish within ${timeoutMs} ms`));
|
|
1461
|
+
this.sourceReloadQueue = run.catch(() => undefined);
|
|
1462
|
+
return run;
|
|
1463
|
+
}
|
|
1464
|
+
async reloadPackageSource(pkg) {
|
|
1405
1465
|
var _a, _b, _c, _d, _e;
|
|
1406
1466
|
const disposeWithTimeout = async (p, label) => {
|
|
1407
1467
|
if (!(p === null || p === void 0 ? void 0 : p.dispose))
|