Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/25467-e2-1a-metadata-list-reads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/metadata-protocol': patch
---

refactor(metadata-protocol): the metadata list reads, the audit read and the UI-view read move out of `protocol.ts` through the protocol host

Clause-②: no
Metadata-protocol: none

No behaviour change. The bodies of `getMetaItems`, `getMetaItemsForExecution`, `getMetaItemsPerPackage`, `readFlattenedMetaItems`, `auditMetaItem` and `getUiView` now live in `protocol-meta-list-reads.ts` and `protocol-meta-audit-ui-view.ts`, reached through the per-instance host (`protocol-host.ts`). Each method keeps its docblock and signature on `ObjectStackProtocolImplementation`, and each moved body differs from the method body only by `this.` reading `host.`. The published `index.d.ts` (ESM and CJS) is byte-identical to the one `main` builds before this change. Nothing to migrate.
48 changes: 39 additions & 9 deletions packages/metadata-protocol/scripts/host-split.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export function planMove(source, member, moduleFile, className = CLASS_NAME) {
if (!cls) return { refusals: [{ line: 0, reason: `no class ${className}` }] };
const method = methodOf(cls, member);
if (!method) return { refusals: [{ line: 0, reason: `no instance method ${member} with a body` }] };
const already = stubKindOf(method);
if (already) return { refusals: [{ line: 0, reason: `already a stub ('${already}'): its body moved before` }] };
const table = memberTable(cls);
const { sites, refusals, used } = rewriteSites(sf, method, table, className);
const sig = signatureParts(sf, method);
Expand Down Expand Up @@ -526,6 +528,41 @@ export function checkMove({ baseSource, headSource, moduleSource, moduleFile, me

// ── Survey ──────────────────────────────────────────────────────────────────

/**
* The function a per-member stub calls: `fn` in `fn.call(host, …)`, and in the
* generic spelling `(fn<R>).call(host, …)` (an instantiation expression in
* parentheses), which the per-member series wrote for a generic method.
*/
function calleeOf(node) {
let n = node;
while (ts.isParenthesizedExpression(n) || ts.isExpressionWithTypeArguments(n)) n = n.expression;
return n;
}

/**
* Is this method already a stub — its body moved before? One of three shapes,
* its last statement returning (awaited or not):
* 'call' — `fn.call(host, …)` or `(fn<R>).call(host, …)` (the per-member series);
* 'host' — `fn(protocolHost(this), …)` (this tool);
* 'plain' — `fn(<its own parameters, in order>)`, `fn` named as the method:
* a body that read no member moved as a plain function.
*/
export function stubKindOf(m) {
const last = m.body?.statements[m.body.statements.length - 1];
let call = last && ts.isReturnStatement(last) ? last.expression : undefined;
if (call && ts.isAwaitExpression(call)) call = call.expression;
if (!call || !ts.isCallExpression(call)) return undefined;
if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'call'
&& ts.isIdentifier(calleeOf(call.expression.expression)) && !!call.arguments[0] && ts.isIdentifier(call.arguments[0])) return 'call';
if (call.arguments[0] && ts.isCallExpression(call.arguments[0]) && ts.isIdentifier(call.arguments[0].expression)
&& call.arguments[0].expression.text === HOST_OF) return 'host';
const name = nameOf(m);
const params = m.parameters.map((p) => (ts.isIdentifier(p.name) ? p.name.text : undefined));
if (ts.isIdentifier(call.expression) && call.expression.text === name && call.arguments.length === params.length
&& call.arguments.every((x, i) => ts.isIdentifier(x) && x.text === params[i])) return 'plain';
return undefined;
}

/** Every instance method with a body: its line count, sites and refusals under the set. */
export function survey(source, className = CLASS_NAME) {
const sf = parse('protocol.ts', source);
Expand All @@ -536,14 +573,7 @@ export function survey(source, className = CLASS_NAME) {
if (!ts.isMethodDeclaration(m) || !m.body || hasModifier(m, ts.SyntaxKind.StaticKeyword)) continue;
const name = nameOf(m);
const { sites, refusals } = rewriteSites(sf, m, table, className);
const last = m.body.statements[m.body.statements.length - 1];
let call = last && ts.isReturnStatement(last) ? last.expression : undefined;
if (call && ts.isAwaitExpression(call)) call = call.expression;
const stub = !!call && ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression)
&& call.expression.name.text === 'call' && ts.isIdentifier(call.expression.expression)
&& !!call.arguments[0] && ts.isIdentifier(call.arguments[0]) ? 'call'
: !!call && ts.isCallExpression(call) && call.arguments[0] && ts.isCallExpression(call.arguments[0])
&& ts.isIdentifier(call.arguments[0].expression) && call.arguments[0].expression.text === HOST_OF ? 'host' : undefined;
const stub = stubKindOf(m);
const lines = sf.getLineAndCharacterOfPosition(m.getEnd()).line - sf.getLineAndCharacterOfPosition(m.getStart(sf)).line + 1;
rows.push({ name, lines, sites: sites.length, refusals, stub });
}
Expand Down Expand Up @@ -593,7 +623,7 @@ function main(argv) {
const all = survey(readFileSync(PROTOCOL, 'utf8'));
const { clashes } = all;
const stubs = all.rows.filter((r) => r.stub);
console.log(`already stubs: ${stubs.length} (${stubs.filter((r) => r.stub === 'call').length} by \`fn.call(host, …)\` from the per-member series, ${stubs.filter((r) => r.stub === 'host').length} by \`fn(${HOST_OF}(this), …)\`)`);
console.log(`already stubs: ${stubs.length} (${stubs.filter((r) => r.stub === 'call').length} by \`fn.call(host, …)\` from the per-member series, ${stubs.filter((r) => r.stub === 'host').length} by \`fn(${HOST_OF}(this), …)\`, ${stubs.filter((r) => r.stub === 'plain').length} by a plain \`fn(<params>)\`)`);
const rows = all.rows.filter((r) => !r.stub);
const movable = rows.filter((r) => !r.refusals.length);
const reasons = new Map();
Expand Down
12 changes: 12 additions & 0 deletions packages/metadata-protocol/scripts/host-split.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ line two \${k}\`;
destructured({ a }: { a: number }) { return a + this.count; }
hostAsPropertyName() { return { host: this.count }.host; }
stubbed(k: string) { const self = { count: this.count }; return stubbedBody.call(self, k); }
private plainStub(k: string, n: number) { return plainStub(k, n); }
stubbedGeneric<R>(k: R) { const host = { count: this.count }; return (stubbedGenericBody<R>).call(host, k); }
}
`;

Expand Down Expand Up @@ -150,6 +152,11 @@ describe('host-split: the move', () => {
expect(plan.fnText).toBeUndefined();
});

it.each(['stubbed', 'stubbedGeneric', 'plainStub'])('refuses to move %s again: it is already a stub', (member) => {
const plan = planMove(BASE, member, 'protocol-target.ts');
expect(plan.refusals.map((r) => r.reason).join(' | ')).toContain('already a stub');
});

it('does not refuse `host` as a property name', () => {
expect(planMove(BASE, 'hostAsPropertyName', 'protocol-target.ts').refusals).toEqual([]);
});
Expand All @@ -159,6 +166,11 @@ describe('host-split: the move', () => {
const by = Object.fromEntries(rows.map((r) => [r.name, r]));
expect(clashes).toEqual([]);
expect(by.stubbed.stub).toBe('call');
// The per-member series' generic spelling `(fn<R>).call(host, …)` is a stub too,
// not a body that binds `host`.
expect(by.stubbedGeneric.stub).toBe('call');
// A body that read no member and moved as a plain function: `return fn(<params>)`.
expect(by.plainStub.stub).toBe('plain');
expect(by.target.refusals).toEqual([]);
expect(by.bareThis.refusals).toHaveLength(1);
const { head } = move();
Expand Down
26 changes: 26 additions & 0 deletions packages/metadata-protocol/src/protocol-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,36 @@ import type { ObjectStackProtocolImplementation } from './protocol.js';
* the class's static block.
*/
export interface ProtocolHost {
readonly artifactLockLayerAt: ObjectStackProtocolImplementation['artifactLockLayerAt'];
readonly baseServesBundleMember: ObjectStackProtocolImplementation['baseServesBundleMember'];
readonly convertStoredItem: ObjectStackProtocolImplementation['convertStoredItem'];
readonly declinesStoredRow: ObjectStackProtocolImplementation['declinesStoredRow'];
readonly disabledActionsOf: ObjectStackProtocolImplementation['disabledActionsOf'];
readonly engine: ObjectStackProtocolImplementation['engine'];
readonly environmentId: ObjectStackProtocolImplementation['environmentId'];
readonly expandStoredViewContainers: ObjectStackProtocolImplementation['expandStoredViewContainers'];
readonly findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows'];
readonly flowRegistrationsOf: ObjectStackProtocolImplementation['flowRegistrationsOf'];
readonly foldObjectExtendersFromRegistry: ObjectStackProtocolImplementation['foldObjectExtendersFromRegistry'];
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
readonly governServedObject: ObjectStackProtocolImplementation['governServedObject'];
readonly hydrateOverlayIntoRegistry: ObjectStackProtocolImplementation['hydrateOverlayIntoRegistry'];
readonly isStoredEntryOfDeclinedName: ObjectStackProtocolImplementation['isStoredEntryOfDeclinedName'];
readonly isUnservedSealedOverlay: ObjectStackProtocolImplementation['isUnservedSealedOverlay'];
readonly isWritablePackage: ObjectStackProtocolImplementation['isWritablePackage'];
readonly lookupArtifactItem: ObjectStackProtocolImplementation['lookupArtifactItem'];
readonly namesWithOwnStoredRow: ObjectStackProtocolImplementation['namesWithOwnStoredRow'];
readonly overrideWriteVerdict: ObjectStackProtocolImplementation['overrideWriteVerdict'];
readonly packagedArtifactOwner: ObjectStackProtocolImplementation['packagedArtifactOwner'];
readonly packagedBaseRefusal: ObjectStackProtocolImplementation['packagedBaseRefusal'];
readonly readActiveOverlayRows: ObjectStackProtocolImplementation['readActiveOverlayRows'];
readonly readFlattenedMetaItems: ObjectStackProtocolImplementation['readFlattenedMetaItems'];
readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned'];
readonly shippedArtifactsOf: ObjectStackProtocolImplementation['shippedArtifactsOf'];
readonly shippingPackagesOf: ObjectStackProtocolImplementation['shippingPackagesOf'];
readonly shipsBundleName: ObjectStackProtocolImplementation['shipsBundleName'];
readonly storedFlowBindingAgrees: ObjectStackProtocolImplementation['storedFlowBindingAgrees'];
readonly storedOverlayEntries: ObjectStackProtocolImplementation['storedOverlayEntries'];
readonly uninstalledNamedBaseRefusal: ObjectStackProtocolImplementation['uninstalledNamedBaseRefusal'];
}

Expand Down
Loading
Loading