Skip to content

Commit 34b0e4a

Browse files
committed
refactor(metadata-protocol): move saveConflict and listLiveMetadataTypes out of protocol.ts into their own modules (#25467 cut 24)
Move-only, shape B. saveConflict (the save door's 409 for a ConflictError, with its decision-audit row) moves to protocol-save-conflict.ts; listLiveMetadataTypes (the SchemaRegistry's types and the metadata service's, de-duplicated) moves to protocol-live-metadata-types.ts. Each class member keeps its name, docblock and signature as a stub that hands a host to the moved function. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent fe53769 commit 34b0e4a

3 files changed

Lines changed: 152 additions & 31 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 24] The live metadata-type listing of
5+
* `ObjectStackProtocolImplementation` (`listLiveMetadataTypes`), moved out
6+
* of `protocol.ts` whole: the types the engine's SchemaRegistry holds, plus
7+
* the types the `metadata` service reports, de-duplicated — the one answer to
8+
* "which types exist here" that `getMetaTypes` and every other caller share.
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same two sources in the
13+
* same order, the same tolerance of an absent `metadata` service, and the
14+
* same de-duplication.
15+
*
16+
* ## How the moved body reaches the class
17+
*
18+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
19+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
20+
* The class keeps the method — the published `dist/*.d.ts` lists its name,
21+
* docblock and declared signature — and its body is one literal of exactly
22+
* the {@link LiveMetadataTypesHost} members it reads, plus a `.call`. Both
23+
* are handed over as getters, so the body reads the protocol's own `engine`
24+
* and its constructor-supplied `getServicesRegistry` field at call time, as
25+
* `protocol-metadata-service-read.ts` hands that field over. Every other
26+
* reader of the method keeps calling the class method.
27+
*
28+
* The body reads none of `protocol.ts`'s module-level declarations and
29+
* imports nothing but the class's type. Nothing re-exports this module, so
30+
* the published surface does not move.
31+
*/
32+
33+
import type { ObjectStackProtocolImplementation } from './protocol.js';
34+
35+
/**
36+
* The `ObjectStackProtocolImplementation` members the function below reads —
37+
* every one of them, and no other; the function's `this` is the `Pick` of the
38+
* members it reads. Each is typed AS the class member (an indexed access on
39+
* the class), so a change to a member's signature on the class is a change
40+
* here too, and naming a member the class does not have is a compile error.
41+
*/
42+
export interface LiveMetadataTypesHost {
43+
readonly engine: ObjectStackProtocolImplementation['engine'];
44+
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
45+
}
46+
47+
/** The body of `ObjectStackProtocolImplementation#listLiveMetadataTypes`; the method's docblock there is its contract. */
48+
export async function listLiveMetadataTypes(
49+
this: Pick<LiveMetadataTypesHost, 'engine' | 'getServicesRegistry'>,
50+
): Promise<string[]> {
51+
const schemaTypes = this.engine.registry.getRegisteredTypes();
52+
53+
// Also include types from MetadataService (runtime-registered: agent, tool, etc.)
54+
let runtimeTypes: string[] = [];
55+
try {
56+
const services = this.getServicesRegistry?.();
57+
const metadataService = services?.get('metadata');
58+
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
59+
runtimeTypes = await metadataService.getRegisteredTypes();
60+
}
61+
} catch {
62+
// MetadataService not available
63+
}
64+
65+
return Array.from(new Set([...schemaTypes, ...runtimeTypes]));
66+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 24] The save door's answer to an optimistic-save conflict, from
5+
* `ObjectStackProtocolImplementation` (`saveConflict`), moved out of
6+
* `protocol.ts` whole: the 409 for a `ConflictError` — the repository's race,
7+
* or a caller's token the keyed comparison refused — built by the one
8+
* conflict builder, plus its decision-audit row (#21207, #23406).
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same refusal text and
13+
* first-write pin, the same audit row, in the same order.
14+
*
15+
* ## How the moved body reaches the class
16+
*
17+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
18+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
19+
* The class keeps the method — the published `dist/*.d.ts` lists its name,
20+
* docblock and declared signature — and its body is one literal of exactly
21+
* the {@link SaveConflictHost} members it reads, each handed over as a
22+
* closure, plus a `.call`. Every other reader of the method keeps calling
23+
* the class method.
24+
*
25+
* What the body reads from elsewhere, it imports from where `protocol.ts`
26+
* imported it. Nothing re-exports this module, so the published surface does
27+
* not move.
28+
*/
29+
30+
import type { ConflictError } from '@objectstack/metadata-core';
31+
import type { ObjectStackProtocolImplementation } from './protocol.js';
32+
33+
/**
34+
* The `ObjectStackProtocolImplementation` members the function below reads —
35+
* every one of them, and no other; the function's `this` is the `Pick` of the
36+
* members it reads. Each is typed AS the class member (an indexed access on
37+
* the class), so a change to a member's signature on the class is a change
38+
* here too, and naming a member the class does not have is a compile error.
39+
*/
40+
export interface SaveConflictHost {
41+
readonly metadataConflictRefusal: ObjectStackProtocolImplementation['metadataConflictRefusal'];
42+
readonly recordOptimisticConflictAudit: ObjectStackProtocolImplementation['recordOptimisticConflictAudit'];
43+
}
44+
45+
/** The body of `ObjectStackProtocolImplementation#saveConflict`; the method's docblock there is its contract. */
46+
export async function saveConflict(
47+
this: Pick<SaveConflictHost, 'metadataConflictRefusal' | 'recordOptimisticConflictAudit'>,
48+
err: ConflictError,
49+
request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' },
50+
writeSource: string,
51+
): Promise<Error> {
52+
const conflict = await this.metadataConflictRefusal(
53+
err,
54+
`${request.type}/${request.name}`,
55+
`${request.type}/${request.name} has been modified since you loaded it.`,
56+
request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined,
57+
);
58+
await this.recordOptimisticConflictAudit({
59+
type: request.type,
60+
name: request.name,
61+
operation: 'save',
62+
...(request.actor ? { actor: request.actor } : {}),
63+
source: writeSource,
64+
expectedParent: err.expectedParent,
65+
actualHead: err.actualHead,
66+
});
67+
return conflict;
68+
}

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ import { reportSealedOverlayRows } from './protocol-sealed-overlay-report.js';
413413
import { expandUnderOwnName, resolveRowlessExpandedView } from './protocol-view-expansion-names.js';
414414
// [#25467 cut 23] The read-side conversion of a stored item: the ADR-0087 chain with its notices, and the warned-once read with the page `type` default.
415415
import { convertStoredItem, convertStoredItemDetailed } from './protocol-stored-item-conversion.js';
416+
// [#25467 cut 24] The live metadata-type listing: the SchemaRegistry's types and the `metadata` service's, de-duplicated.
417+
import { listLiveMetadataTypes } from './protocol-live-metadata-types.js';
418+
// [#25467 cut 24] The save door's 409 for a `ConflictError`, with its decision-audit row.
419+
import { saveConflict } from './protocol-save-conflict.js';
416420

417421
/**
418422
* Canonical Zod schema per metadata type lives in
@@ -8492,21 +8496,14 @@ export class ObjectStackProtocolImplementation implements
84928496
* One accessor, no second vocabulary.
84938497
*/
84948498
private async listLiveMetadataTypes(): Promise<string[]> {
8495-
const schemaTypes = this.engine.registry.getRegisteredTypes();
8496-
8497-
// Also include types from MetadataService (runtime-registered: agent, tool, etc.)
8498-
let runtimeTypes: string[] = [];
8499-
try {
8500-
const services = this.getServicesRegistry?.();
8501-
const metadataService = services?.get('metadata');
8502-
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
8503-
runtimeTypes = await metadataService.getRegisteredTypes();
8504-
}
8505-
} catch {
8506-
// MetadataService not available
8507-
}
8508-
8509-
return Array.from(new Set([...schemaTypes, ...runtimeTypes]));
8499+
// [#25467 cut 24] The body moved to `protocol-live-metadata-types.ts` (see `LiveMetadataTypesHost`).
8500+
const readEngine = () => this.engine;
8501+
const readGetServicesRegistry = () => this.getServicesRegistry;
8502+
const host: ThisParameterType<typeof listLiveMetadataTypes> = {
8503+
get engine() { return readEngine(); },
8504+
get getServicesRegistry() { return readGetServicesRegistry(); },
8505+
};
8506+
return listLiveMetadataTypes.call(host);
85108507
}
85118508

85128509
async getMetaTypes() {
@@ -21471,22 +21468,12 @@ export class ObjectStackProtocolImplementation implements
2147121468
request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' },
2147221469
writeSource: string,
2147321470
): Promise<Error> {
21474-
const conflict = await this.metadataConflictRefusal(
21475-
err,
21476-
`${request.type}/${request.name}`,
21477-
`${request.type}/${request.name} has been modified since you loaded it.`,
21478-
request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined,
21479-
);
21480-
await this.recordOptimisticConflictAudit({
21481-
type: request.type,
21482-
name: request.name,
21483-
operation: 'save',
21484-
...(request.actor ? { actor: request.actor } : {}),
21485-
source: writeSource,
21486-
expectedParent: err.expectedParent,
21487-
actualHead: err.actualHead,
21488-
});
21489-
return conflict;
21471+
// [#25467 cut 24] The body moved to `protocol-save-conflict.ts` (see `SaveConflictHost`).
21472+
const host: ThisParameterType<typeof saveConflict> = {
21473+
metadataConflictRefusal: (...a) => this.metadataConflictRefusal(...a),
21474+
recordOptimisticConflictAudit: (...a) => this.recordOptimisticConflictAudit(...a),
21475+
};
21476+
return saveConflict.call(host, err, request, writeSource);
2149021477
}
2149121478

2149221479
/**

0 commit comments

Comments
 (0)