@feasibleone/blong-gogo 1.35.0 → 1.37.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 +27 -0
- package/package.json +4 -3
- package/src/AdapterBase.ts +33 -5
- package/src/Gateway.ts +14 -16
- package/src/Registry.ts +15 -2
- package/src/RpcClient.ts +3 -3
- package/src/RpcServer.ts +2 -2
- package/src/SemanticLog.test.ts +67 -8
- package/src/SemanticLog.ts +1 -1
- package/src/Watch.ts +33 -4
- package/src/adapterProgress.test.ts +190 -0
- package/src/browserEagerImports.test.ts +62 -0
- package/src/chain.allure.test.ts +216 -0
- package/src/chain.ts +238 -2
- package/src/checkpoint.test.ts +265 -0
- package/src/checkpoint.ts +115 -14
- package/src/ci.intent.test.ts +94 -0
- package/src/codec/adapter/jsonrpc/receive.ts +4 -4
- package/src/codec/adapter/mle/ready.ts +39 -11
- package/src/load.ts +133 -20
- package/src/mle.ts +3 -3
- package/src/orchestrator/common/dispatch.ts +6 -5
- package/src/pino-cacache.ts +59 -3
- package/src/pino-pretty.ts +1 -1
- package/src/registry.lib.test.ts +97 -0
- package/src/semanticContext.test.ts +125 -35
- package/src/semanticContext.ts +51 -22
package/src/load.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type SolutionFactory,
|
|
19
19
|
} from '@feasibleone/blong/types';
|
|
20
20
|
|
|
21
|
-
import {withProgress} from '@feasibleone/blong-lib';
|
|
21
|
+
import {SLOW_STEP_MS, withProgress} from '@feasibleone/blong-lib';
|
|
22
22
|
import {WELL_KNOWN_LAYERS} from '@feasibleone/blong-lib/layers';
|
|
23
23
|
import {Type, type TSchema} from 'typebox';
|
|
24
24
|
import merge from 'ut-function.merge';
|
|
@@ -216,6 +216,42 @@ async function discoverRealmTestMethods(
|
|
|
216
216
|
return methods;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The margin a step of a load is measured against when the realm names none.
|
|
221
|
+
*
|
|
222
|
+
* Read off the log config (`log.slowMs`) so a realm or a runner can raise it for
|
|
223
|
+
* a machine that is legitimately slow, and so one number tunes both halves of a
|
|
224
|
+
* delayed start: the loader's steps and the retention store's own.
|
|
225
|
+
*/
|
|
226
|
+
function slowStepMs(log: unknown): number {
|
|
227
|
+
const configured = (log as {slowMs?: unknown} | undefined)?.slowMs;
|
|
228
|
+
return typeof configured === 'number' && configured >= 0 ? configured : SLOW_STEP_MS;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A logger for the steps that run before the realm's own log exists.
|
|
233
|
+
*
|
|
234
|
+
* A root load builds the log component first, and until it is up there is nothing
|
|
235
|
+
* to log through — which is exactly when a slow step is least visible and most
|
|
236
|
+
* confusing, because the process prints nothing at all for as long as it takes.
|
|
237
|
+
* This writes to stderr, where the framework's other startup messages go, and
|
|
238
|
+
* stays silent in a platform that has no process (the browser bundle). Only
|
|
239
|
+
* `warn` is answered: a step that is not slow has nothing to say, and the
|
|
240
|
+
* loader's ordinary progress belongs in the log the realm is about to have.
|
|
241
|
+
*/
|
|
242
|
+
function bootstrapLogger(): {warn: (...args: unknown[]) => void} {
|
|
243
|
+
return {
|
|
244
|
+
// Pino-shaped, like every other logger `withProgress` is handed: the
|
|
245
|
+
// details object first, the message second.
|
|
246
|
+
warn: (...args: unknown[]) => {
|
|
247
|
+
const [details, message] = args as [Record<string, unknown> | undefined, string];
|
|
248
|
+
const stderr = (globalThis as {process?: {stderr?: {write: (text: string) => void}}})
|
|
249
|
+
.process?.stderr;
|
|
250
|
+
stderr?.write(`warn blong ${message} ${JSON.stringify(details ?? {})}\n`);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
const System: symbol = Symbol('system');
|
|
220
256
|
|
|
221
257
|
export function system(original: {[key: symbol]: boolean}): void {
|
|
@@ -232,12 +268,24 @@ function activeConfigs<T extends TSchema>(
|
|
|
232
268
|
platformConfigs: string[],
|
|
233
269
|
): (boolean | object)[] {
|
|
234
270
|
return (
|
|
235
|
-
(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
(
|
|
272
|
+
(['default'] as string[])
|
|
273
|
+
.concat(configNames)
|
|
274
|
+
.concat(platformConfigs)
|
|
275
|
+
// A CI run *is* a captured run, so the `ci` block applies whether or not the
|
|
276
|
+
// entry named the intent — the same reason `exit` is derived from `isCI()`
|
|
277
|
+
// in the synthetic config. Which entry happened to know about CI decided
|
|
278
|
+
// whether a package's tap leg wrote Allure results at all, and a package
|
|
279
|
+
// whose only runner is tap then had no report to link.
|
|
280
|
+
//
|
|
281
|
+
// Named last, which is where a caller that passes it puts it: the Playwright
|
|
282
|
+
// webServer passes `ci` after `playwright` so its colours win over the block's
|
|
283
|
+
// `log.color = false`.
|
|
284
|
+
.concat(isCI() ? ['ci'] : [])
|
|
285
|
+
.map(name => (mod.config as unknown as Record<string, unknown>)?.[name])
|
|
286
|
+
.filter(Boolean) as (boolean | object)[]
|
|
287
|
+
).concat({pkg: mod.pkg, children: mod.children, url: mod.url})
|
|
288
|
+
);
|
|
241
289
|
}
|
|
242
290
|
|
|
243
291
|
/** A thenable value that can be externally resolved/rejected. */
|
|
@@ -449,7 +497,15 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
449
497
|
// always false, and every framework realm is silently absent: the gateway then
|
|
450
498
|
// refuses every unauthenticated route (no login realm) and RBAC resolves nothing.
|
|
451
499
|
const isPlatformRoot = api === undefined;
|
|
452
|
-
const
|
|
500
|
+
const bootstrap = bootstrapLogger();
|
|
501
|
+
// A factory may answer with the module or with a promise for it, so the
|
|
502
|
+
// measurement wraps whatever it answered rather than assuming the shape.
|
|
503
|
+
const mod = await withProgress(
|
|
504
|
+
bootstrap,
|
|
505
|
+
`suite ${name}`,
|
|
506
|
+
Promise.resolve(def({type: Type, manifest})),
|
|
507
|
+
{slowMs: SLOW_STEP_MS},
|
|
508
|
+
);
|
|
453
509
|
// Record the realm's own file in the tree's set, so a realm reached twice -
|
|
454
510
|
// once as a suite's child and once by the framework's `frameworkRealms` - is
|
|
455
511
|
// recognised the second time. A realm answers with its *factory*, so its url
|
|
@@ -489,6 +545,26 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
489
545
|
default: {
|
|
490
546
|
watch: {
|
|
491
547
|
test: [],
|
|
548
|
+
/**
|
|
549
|
+
* Allure reporting for handler tests. Off by default: a report is
|
|
550
|
+
* something a run is asked for, which is what `--watch.allure.enabled=true`
|
|
551
|
+
* on the blong CLI does for a dev run and what the `ci` block below does
|
|
552
|
+
* for a captured one. `blong-allure` is a dependency of this package, so
|
|
553
|
+
* the CLI that turns the results into HTML is the one they were written
|
|
554
|
+
* for rather than whatever is on the runner's PATH.
|
|
555
|
+
*
|
|
556
|
+
* The results have a directory of their own because a package can report
|
|
557
|
+
* to Allure from two producers — the browser tests of a realm and its
|
|
558
|
+
* handler tests — and each clears the directory it writes to before a run.
|
|
559
|
+
* `blong-dev` merges every producer's directory into the one report a
|
|
560
|
+
* package publishes.
|
|
561
|
+
*/
|
|
562
|
+
allure: {
|
|
563
|
+
enabled: false,
|
|
564
|
+
outputDir: 'allure-results-tap',
|
|
565
|
+
historyPath: '.allure/history.jsonl',
|
|
566
|
+
generateOnEnd: false,
|
|
567
|
+
},
|
|
492
568
|
},
|
|
493
569
|
// The server's log implementation. The browser platform
|
|
494
570
|
// keeps its own logger regardless: the loader checks the
|
|
@@ -522,7 +598,7 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
522
598
|
// The semantic implementation's store, and the pino
|
|
523
599
|
// implementation's cache beside it: they share one
|
|
524
600
|
// directory and one key space, so a
|
|
525
|
-
// `
|
|
601
|
+
// `semlog://r/<id>` reference resolves
|
|
526
602
|
// whichever implementation wrote the entry.
|
|
527
603
|
cache: {
|
|
528
604
|
dir: '~/.blong/log-cache',
|
|
@@ -606,9 +682,22 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
606
682
|
/**
|
|
607
683
|
* A run whose output is captured rather than watched. The
|
|
608
684
|
* Playwright webServer passes this last in CI, so it wins over the
|
|
609
|
-
* `playwright` block's colours.
|
|
685
|
+
* `playwright` block's colours. A captured run is also the run whose
|
|
686
|
+
* report nobody is watching, so its Allure results are written and
|
|
687
|
+
* its HTML report generated at the end.
|
|
688
|
+
*
|
|
689
|
+
* On CI the block applies whether or not the intent was named: the
|
|
690
|
+
* environment already says the output is captured, and an entry that
|
|
691
|
+
* has to remember to pass `ci` for its package to have a report is a
|
|
692
|
+
* trap for the next package (`activeConfigs`).
|
|
610
693
|
*/
|
|
611
|
-
ci: {
|
|
694
|
+
ci: {
|
|
695
|
+
log: {color: false},
|
|
696
|
+
// Results, not a report: `blong-dev` turns every producer's results
|
|
697
|
+
// into the one report a package publishes, so generating here would
|
|
698
|
+
// be the same work again, into a directory nobody uploads.
|
|
699
|
+
watch: {allure: {enabled: true}},
|
|
700
|
+
},
|
|
612
701
|
/**
|
|
613
702
|
* A deployed process never runs the service itself: the
|
|
614
703
|
* service is deployed once, beside the processes whose
|
|
@@ -806,7 +895,7 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
806
895
|
});
|
|
807
896
|
}
|
|
808
897
|
loadedConfigs.push(...activeConfigs(mod, configNames, platformApi.configs));
|
|
809
|
-
const
|
|
898
|
+
const configPromise = platformApi.loadConfig(
|
|
810
899
|
{
|
|
811
900
|
name,
|
|
812
901
|
pkg: {name, version: '0.0.0'},
|
|
@@ -834,6 +923,14 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
834
923
|
parentConfig,
|
|
835
924
|
loadedConfigs.filter(Boolean) as object[],
|
|
836
925
|
);
|
|
926
|
+
// The config is what says how slow a step may be (`log.slowMs`), so it cannot
|
|
927
|
+
// itself be measured against a configured margin — only against the default.
|
|
928
|
+
const {loadedConfig: mergedConfig, configRuntime} = await withProgress(
|
|
929
|
+
bootstrap,
|
|
930
|
+
`config ${name}`,
|
|
931
|
+
configPromise,
|
|
932
|
+
{slowMs: SLOW_STEP_MS},
|
|
933
|
+
);
|
|
837
934
|
|
|
838
935
|
// Populate the manifest from config values (e.g. `--manifest.gatewayPort=8080`
|
|
839
936
|
// parsed by blong-config into `mergedConfig.manifest.gatewayPort`).
|
|
@@ -866,6 +963,12 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
866
963
|
context: `${defKind}`,
|
|
867
964
|
},
|
|
868
965
|
);
|
|
966
|
+
// From here on every step of the load is measured against the margin the
|
|
967
|
+
// config names, and a step that crosses it is reported at warn level through
|
|
968
|
+
// whichever logger exists — the realm's own, or the bootstrap one while the
|
|
969
|
+
// log component is still being built.
|
|
970
|
+
const slowMs = slowStepMs(mergedConfig.log);
|
|
971
|
+
const stepLog = () => logger ?? bootstrap;
|
|
869
972
|
if (typeof parentConfig === 'string' && mergedConfig.watch)
|
|
870
973
|
mergedConfig.watch.configs = mergedConfig.configs;
|
|
871
974
|
|
|
@@ -891,11 +994,11 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
891
994
|
const explicitChildren = new Set(
|
|
892
995
|
children.filter(c => typeof c === 'string').map(c => platformApi.basename(c as string)),
|
|
893
996
|
);
|
|
894
|
-
const discoveredFolders = await
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
rootKind,
|
|
898
|
-
|
|
997
|
+
const discoveredFolders = await withProgress(
|
|
998
|
+
stepLog(),
|
|
999
|
+
`discover layers of ${name}`,
|
|
1000
|
+
discoverLayerFolders(platformApi, base, rootKind, explicitChildren),
|
|
1001
|
+
{slowMs},
|
|
899
1002
|
);
|
|
900
1003
|
for (const [folderName, activation] of discoveredFolders) {
|
|
901
1004
|
if (!(folderName in mergedConfig))
|
|
@@ -1300,12 +1403,22 @@ export default async function loadRealm<T extends TSchema>(
|
|
|
1300
1403
|
);
|
|
1301
1404
|
item = async () => loaded.filter(Boolean);
|
|
1302
1405
|
}
|
|
1303
|
-
|
|
1406
|
+
// The import and the start are separate steps with separate costs: an
|
|
1407
|
+
// import is the module graph, which is slow on a cold filesystem, while
|
|
1408
|
+
// a start is the component's own init. A delayed start that names only
|
|
1409
|
+
// "load log" would not say which of the two to look at, so the two are
|
|
1410
|
+
// reported under labels that do.
|
|
1411
|
+
const loadedModules = await withProgress(
|
|
1412
|
+
stepLog(),
|
|
1413
|
+
`import ${itemName}`,
|
|
1414
|
+
(item as () => Promise<unknown[]>)(),
|
|
1415
|
+
{slowMs},
|
|
1416
|
+
);
|
|
1304
1417
|
const modules = Array.isArray(loadedModules) ? loadedModules : [loadedModules];
|
|
1305
1418
|
let loadedCount = 0;
|
|
1306
1419
|
await withProgress(
|
|
1307
|
-
|
|
1308
|
-
`
|
|
1420
|
+
stepLog(),
|
|
1421
|
+
`start ${itemName}`,
|
|
1309
1422
|
(async () => {
|
|
1310
1423
|
for (const module of modules) {
|
|
1311
1424
|
const item = await module;
|
package/src/mle.ts
CHANGED
|
@@ -94,7 +94,7 @@ export default fp<IConfig>(async function mlePlugin(fastify: FastifyInstance, co
|
|
|
94
94
|
jsonrpc?: unknown;
|
|
95
95
|
result?: Record<string, unknown>;
|
|
96
96
|
error?: Record<string, unknown>;
|
|
97
|
-
|
|
97
|
+
progress?: unknown;
|
|
98
98
|
},
|
|
99
99
|
) => {
|
|
100
100
|
if (isPublic(request.originalUrl)) return payload;
|
|
@@ -119,7 +119,7 @@ export default fp<IConfig>(async function mlePlugin(fastify: FastifyInstance, co
|
|
|
119
119
|
id: undefined,
|
|
120
120
|
jsonrpc: undefined,
|
|
121
121
|
error: undefined,
|
|
122
|
-
|
|
122
|
+
progress: undefined,
|
|
123
123
|
};
|
|
124
124
|
let result,
|
|
125
125
|
error = undefined as string | undefined;
|
|
@@ -141,7 +141,7 @@ export default fp<IConfig>(async function mlePlugin(fastify: FastifyInstance, co
|
|
|
141
141
|
jsonrpc: where.jsonrpc,
|
|
142
142
|
result: result && (await encrypt(Buffer.from(result))),
|
|
143
143
|
error: error && (await encrypt(Buffer.from(error))),
|
|
144
|
-
|
|
144
|
+
progress: where.progress,
|
|
145
145
|
});
|
|
146
146
|
} catch (error) {
|
|
147
147
|
reply.code(400);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {orchestrator, type IMeta} from '@feasibleone/blong/types';
|
|
2
|
-
import {
|
|
2
|
+
import {recordedCall} from '../../lib.ts';
|
|
3
3
|
import {declareCall, namespaceOf} from '../../semanticContext.ts';
|
|
4
4
|
|
|
5
5
|
export default orchestrator<{destination?: string; appendNamespace?: string}>(({remote}) => ({
|
|
@@ -35,10 +35,11 @@ export default orchestrator<{destination?: string; appendNamespace?: string}>(({
|
|
|
35
35
|
// name would credit the hop to whichever namespace happened to be served
|
|
36
36
|
// alongside it.
|
|
37
37
|
//
|
|
38
|
-
// The leg is
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
38
|
+
// The leg is the method the forwarded call reaches its destination with — the
|
|
39
|
+
// wire name, slash and all (`db/gateway.bundle.find`): that is the method the
|
|
40
|
+
// callee strips back to, and a label that dotted it would no longer be the name
|
|
41
|
+
// the call was made by. Who made it is stated beside the method, not inside it.
|
|
42
|
+
const declared = forwarded;
|
|
42
43
|
const forward = async (): Promise<unknown[] | undefined> =>
|
|
43
44
|
(await recordedCall(this, declared, () =>
|
|
44
45
|
remote.dispatch(...params, {...$meta, method: forwarded}),
|
package/src/pino-cacache.ts
CHANGED
|
@@ -44,8 +44,48 @@ export interface CacacheTransportOptions {
|
|
|
44
44
|
const RETENTION_STATE_KEY = '__blong_retention_state__';
|
|
45
45
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* `cacache.rm.entry` with its third argument, which the published types do not
|
|
49
|
+
* declare.
|
|
50
|
+
*
|
|
51
|
+
* `removeFully` is real: `cacache`'s own `lib/rm.js` hands the options to
|
|
52
|
+
* `index.delete`, which removes the key's index file rather than appending a
|
|
53
|
+
* deletion to it. The types shipped for `cacache` 20 model only the two-argument
|
|
54
|
+
* form, so the option is declared here once instead of being cast at the call.
|
|
55
|
+
*/
|
|
56
|
+
const removeEntryFully = cacache.rm.entry as unknown as (
|
|
57
|
+
cachePath: string,
|
|
58
|
+
key: string,
|
|
59
|
+
opts: {removeFully?: boolean},
|
|
60
|
+
) => Promise<unknown>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How long a retention step may take before it is reported.
|
|
64
|
+
*
|
|
65
|
+
* The transport runs in a pino worker thread with no logger of its own, so it
|
|
66
|
+
* writes to stderr — which is where the loader's own messages go, and where a
|
|
67
|
+
* reader of the process's output will see it. Matches the framework's margin for
|
|
68
|
+
* a slow step: a retention pass that took longer is the kind of delay that has
|
|
69
|
+
* cost a start its first seconds.
|
|
70
|
+
*/
|
|
71
|
+
const SLOW_STEP_MS = 1_000;
|
|
72
|
+
|
|
73
|
+
function reportSlow(step: string, started: number, details?: Record<string, unknown>): void {
|
|
74
|
+
const elapsedMs = Date.now() - started;
|
|
75
|
+
if (elapsedMs < SLOW_STEP_MS) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`warn pino-cacache ${step} took ${elapsedMs}ms` +
|
|
80
|
+
(details ? ` ${JSON.stringify(details)}` : '') +
|
|
81
|
+
'\n',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
async function pruneOldEntries(cachePath: string, retentionCount: number): Promise<void> {
|
|
86
|
+
const scanStarted = Date.now();
|
|
48
87
|
const index = await cacache.ls(cachePath);
|
|
88
|
+
reportSlow('index scan', scanStarted, {entries: Object.keys(index).length});
|
|
49
89
|
|
|
50
90
|
// Collect all real log entries (skip the retention-state entry itself)
|
|
51
91
|
const entries = Object.values(index).filter(e => e.key !== RETENTION_STATE_KEY);
|
|
@@ -61,13 +101,25 @@ async function pruneOldEntries(cachePath: string, retentionCount: number): Promi
|
|
|
61
101
|
return ta - tb;
|
|
62
102
|
});
|
|
63
103
|
|
|
104
|
+
const pruneStarted = Date.now();
|
|
64
105
|
const toDelete = entries.slice(0, entries.length - retentionCount);
|
|
65
106
|
for (const entry of toDelete) {
|
|
66
|
-
|
|
107
|
+
// `removeFully` deletes the entry's index file. Without it `cacache`
|
|
108
|
+
// appends a deletion and keeps the file, so the index grows one file per
|
|
109
|
+
// entry ever written — and `cacache.ls` above, which reads all of them,
|
|
110
|
+
// becomes the slowest thing a process does. See
|
|
111
|
+
// `core/semantic-log/src/cache.ts`, which shares this directory and makes
|
|
112
|
+
// the same call.
|
|
113
|
+
await removeEntryFully(cachePath, entry.key, {removeFully: true});
|
|
67
114
|
}
|
|
68
115
|
|
|
69
116
|
// Garbage-collect content that is no longer referenced by any index entry
|
|
117
|
+
const verifyStarted = Date.now();
|
|
70
118
|
await cacache.verify(cachePath);
|
|
119
|
+
reportSlow('retention pass', pruneStarted, {
|
|
120
|
+
pruned: toDelete.length,
|
|
121
|
+
verifyMs: Date.now() - verifyStarted,
|
|
122
|
+
});
|
|
71
123
|
}
|
|
72
124
|
|
|
73
125
|
async function retentionCheckRun(cachePath: string, retentionCount: number): Promise<void> {
|
|
@@ -100,8 +152,12 @@ export default async function transport(options: CacacheTransportOptions) {
|
|
|
100
152
|
const cachePath = resolveHome(rawCachePath);
|
|
101
153
|
|
|
102
154
|
// Run retention check once on transport startup (at most once per day)
|
|
103
|
-
retentionCheckRun(cachePath, retentionCount).catch(() => {
|
|
104
|
-
// Retention errors must not crash the transport
|
|
155
|
+
retentionCheckRun(cachePath, retentionCount).catch((error: unknown) => {
|
|
156
|
+
// Retention errors must not crash the transport — but a retention that
|
|
157
|
+
// fails is why a cache grows without bound, so it is reported rather than
|
|
158
|
+
// swallowed: silently skipping the pass is indistinguishable from running
|
|
159
|
+
// it and finding nothing to do.
|
|
160
|
+
process.stderr.write(`warn pino-cacache retention failed: ${String(error)}\n`);
|
|
105
161
|
});
|
|
106
162
|
|
|
107
163
|
return build(async function (source) {
|
package/src/pino-pretty.ts
CHANGED
|
@@ -46,7 +46,7 @@ export default (options: PinoPretty.PrettyOptions) =>
|
|
|
46
46
|
$meta,
|
|
47
47
|
} = log;
|
|
48
48
|
return [
|
|
49
|
-
id && colors.dim(`
|
|
49
|
+
id && colors.dim(`semlog://r/${id}`),
|
|
50
50
|
context && colors.greenBright(context),
|
|
51
51
|
prefix,
|
|
52
52
|
$meta?.mtid && colors.magenta($meta.mtid),
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `lib` a handler is built with (PRD R26).
|
|
3
|
+
*
|
|
4
|
+
* `blongLib` is spread into `lib` first, so a name the library exports cannot silently
|
|
5
|
+
* replace a handle the framework injects: `blongLib` exports `snapshot` (a chain step) and
|
|
6
|
+
* used to export `checkpoint` for it, an unrelated thing that shares a word with the
|
|
7
|
+
* runtime's milestone handle, so the spread used to sit after the injections. The order
|
|
8
|
+
* is the invariant, so it is pinned here rather than left to a comment — together with
|
|
9
|
+
* the mode-gated `assert`, the other member whose presence the mode decides.
|
|
10
|
+
*/
|
|
11
|
+
import * as blongLib from '@feasibleone/blong-lib';
|
|
12
|
+
import type {ILib} from '@feasibleone/blong/types';
|
|
13
|
+
import {vocabulary} from '@feasibleone/semantic-log/attachable';
|
|
14
|
+
import t from 'tap';
|
|
15
|
+
import Registry from './Registry.ts';
|
|
16
|
+
|
|
17
|
+
// Minimal stand-ins: `_createHandlers` only reads the error factory, the platform's
|
|
18
|
+
// timing and the api schema, and closes over the rest.
|
|
19
|
+
const components = {
|
|
20
|
+
log: {logger: () => ({info() {}, error() {}, warn() {}, debug() {}})},
|
|
21
|
+
error: {register: () => () => undefined},
|
|
22
|
+
rpcServer: {},
|
|
23
|
+
remote: {remote: () => ({})},
|
|
24
|
+
gateway: {},
|
|
25
|
+
local: {},
|
|
26
|
+
resolution: {},
|
|
27
|
+
watch: {},
|
|
28
|
+
apiSchema: {},
|
|
29
|
+
platform: {context: {}, timing: () => undefined},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Run one handler through the factory and hand back the `lib` it was given. */
|
|
33
|
+
async function captureLib(mode: 'test' | 'debug' | 'production'): Promise<ILib> {
|
|
34
|
+
const registry = new Registry({checkpointMode: mode}, components as never);
|
|
35
|
+
let lib: ILib | undefined;
|
|
36
|
+
await (
|
|
37
|
+
registry as unknown as {
|
|
38
|
+
_createHandlers(handlers: unknown, port: object | undefined): Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
)._createHandlers(
|
|
41
|
+
[
|
|
42
|
+
async (params: {lib: ILib}) => {
|
|
43
|
+
lib = params.lib;
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
undefined,
|
|
47
|
+
);
|
|
48
|
+
if (lib === undefined) {
|
|
49
|
+
throw new Error('the handler factory was not called');
|
|
50
|
+
}
|
|
51
|
+
return lib;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
t.test('a handler is handed the framework branch helper, not a name the library owns', async t => {
|
|
55
|
+
const lib = await captureLib('test');
|
|
56
|
+
t.type(lib.decide, 'function', 'the branch helper is always injected');
|
|
57
|
+
t.equal(
|
|
58
|
+
lib.decide,
|
|
59
|
+
vocabulary.decide,
|
|
60
|
+
'and it is the attachable one, so a realm needs no dependency on the log library',
|
|
61
|
+
);
|
|
62
|
+
t.equal(lib.checkpoint, vocabulary.point, 'the milestone handle is the ambient one');
|
|
63
|
+
t.notOk(
|
|
64
|
+
'checkpoint' in blongLib,
|
|
65
|
+
'the library does not define that name, so the spread cannot replace it',
|
|
66
|
+
);
|
|
67
|
+
t.notOk('decide' in blongLib, 'nor the branch helper');
|
|
68
|
+
t.notOk('assert' in blongLib, 'nor the assertion handle');
|
|
69
|
+
t.end();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t.test('checkpoint follows the mode, while decide is always there', async t => {
|
|
73
|
+
t.type((await captureLib('test')).checkpoint, 'function', 'test mode reports points');
|
|
74
|
+
t.type((await captureLib('debug')).checkpoint, 'function', 'debug mode reports them too');
|
|
75
|
+
t.equal(
|
|
76
|
+
(await captureLib('production')).checkpoint,
|
|
77
|
+
undefined,
|
|
78
|
+
'and production leaves the slot empty, so `lib.checkpoint?.()` is one call',
|
|
79
|
+
);
|
|
80
|
+
t.type(
|
|
81
|
+
(await captureLib('production')).decide,
|
|
82
|
+
'function',
|
|
83
|
+
'a branch is selected in production as anywhere else',
|
|
84
|
+
);
|
|
85
|
+
t.end();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
t.test('assert is there only where a failure may be expected', async t => {
|
|
89
|
+
t.type((await captureLib('test')).assert, 'function', 'test mode asserts');
|
|
90
|
+
t.type((await captureLib('debug')).assert, 'function', 'debug mode asserts too');
|
|
91
|
+
t.equal(
|
|
92
|
+
(await captureLib('production')).assert,
|
|
93
|
+
undefined,
|
|
94
|
+
'and production leaves the slot empty, so `lib.assert?.(…)` is one call',
|
|
95
|
+
);
|
|
96
|
+
t.end();
|
|
97
|
+
});
|