feat(steps): cross-tier channels — a client's fetch onto its own route, queue jobs onto consumers, bus and socket events onto handlers
- resolution/tier-synthesizer.ts: http-client (literal fetch/axios/ky/got/$fetch paths, axios.create baseURL instances, template holes as :params, base-URL holes by a two-segment tail; unique match only), queue-job (BullMQ/Bull add ↔ @Process/@Processor, WorkerHost process, new Worker, queue.process), event-bus (EventEmitter2 emit ↔ @OnEvent with globs; socket emit ↔ @SubscribeMessage / socket.on both ways with tier); channel, tier, callee, registeredAt on every edge; generic transport events never pair; test and generated files never sources; registered before the emitter pass
- steps.ts: crossing() reads tier/channel before languages; an endpoint reached across a tier is a bridge box and a boundary like a screen (through=1 enters it); a channel's call is not also an effect; sites read as written; a Next 'use server' action is a crossing by its directive (when.ts directive); a function-valued constant handler (asyncHandler(...)) is a route root and borrows the file-scope calls and refs within its lines
- express.ts: app.use('/prefix', router) mounts composed onto route names in postExtract (nested, by import or require); chained router.route('/x').get(h).put(h2) extracted, across lines
- frameworks/package-deps.ts: dependencies read from workspace package.json files too (Express, React, Expo Router, NestJS detect)
- routing manifest names constant handlers; e2e/ is a test directory; explore's Flow section labels the new channels
- tests: ui-steps-cross-tier (monorepo fixture: Next client + Express/Nest API), servers test updated for the queue landing
- docs: CHANGELOG, spec §3.13 cross-tier paragraph, CLAUDE.md, callback-edge-synthesis.md, plan P3 built
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
co-authored by
Claude Fable 5
parent
950686def4
commit
b1f40c57dd
@@ -10,6 +10,7 @@
|
||||
* Symbol view into a parse of the repository.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import type CodeGraph from '../../index';
|
||||
import type { Language } from '../../types';
|
||||
import {
|
||||
@@ -107,8 +108,20 @@ export interface SiteReader {
|
||||
decorators(definition: { filePath: string; language: Language; startLine: number }): Promise<DefinitionDecorators | null>;
|
||||
/** The declared types of the members of the class a definition belongs to, by member name; empty when unreadable. */
|
||||
memberTypes(definition: { filePath: string; language: Language; startLine: number }): Promise<Map<string, string>>;
|
||||
/**
|
||||
* The `'use server'` / `'use client'` directive a JS-family file opens with,
|
||||
* and whether the definition itself opens with `'use server'` (a server
|
||||
* action declared inline). Nothing for other languages or unreadable files.
|
||||
*/
|
||||
directive(definition: { filePath: string; language: Language; startLine: number }): Promise<{ file: 'server' | 'client' | null; own: boolean }>;
|
||||
}
|
||||
|
||||
const JS_FAMILY: ReadonlySet<string> = new Set(['javascript', 'typescript', 'tsx', 'jsx']);
|
||||
/** A file read for its directives, at most. */
|
||||
const MAX_DIRECTIVE_FILE = 512 * 1024;
|
||||
const FILE_DIRECTIVE = /^(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*(['"])use (server|client)\1/;
|
||||
const OWN_DIRECTIVE = /^\s*(['"])use server\1\s*;?\s*$/m;
|
||||
|
||||
/**
|
||||
* Both readings of one call site — WHEN it runs and WITH WHAT — for the
|
||||
* endpoints that walk chains (Screens, Steps). One file resolution and one
|
||||
@@ -117,6 +130,7 @@ export interface SiteReader {
|
||||
*/
|
||||
export function createSiteReader(cg: CodeGraph, projectRoot: string, maxSites = 600): SiteReader {
|
||||
const files = new Map<string, { abs: string; language: Language } | null>();
|
||||
const texts = new Map<string, string | null>();
|
||||
let sites = 0;
|
||||
const resolve = (caller: { filePath: string; language: Language }): { abs: string; language: Language } | null => {
|
||||
const posix = caller.filePath.replace(/\\/g, '/');
|
||||
@@ -183,6 +197,31 @@ export function createSiteReader(cg: CodeGraph, projectRoot: string, maxSites =
|
||||
if (!file) return new Map();
|
||||
return memberTypesForFile(file.abs, file.language, definition.startLine);
|
||||
},
|
||||
async directive(definition) {
|
||||
// Not counted: a text read, cached per file, no tree.
|
||||
const none = { file: null, own: false } as const;
|
||||
if (!JS_FAMILY.has(definition.language)) return none;
|
||||
const file = resolve(definition);
|
||||
if (!file) return none;
|
||||
let text = texts.get(file.abs);
|
||||
if (text === undefined) {
|
||||
try {
|
||||
text = fs.statSync(file.abs).size <= MAX_DIRECTIVE_FILE ? fs.readFileSync(file.abs, 'utf8') : null;
|
||||
} catch {
|
||||
text = null;
|
||||
}
|
||||
texts.set(file.abs, text);
|
||||
}
|
||||
if (text === null) return none;
|
||||
const head = FILE_DIRECTIVE.exec(text);
|
||||
const fileDirective = head ? (head[2] as 'server' | 'client') : null;
|
||||
let own = false;
|
||||
if (definition.startLine > 0) {
|
||||
const lines = text.split('\n');
|
||||
own = OWN_DIRECTIVE.test(lines.slice(definition.startLine - 1, definition.startLine + 3).join('\n'));
|
||||
}
|
||||
return { file: fileDirective, own };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user