From 34b0e4a3c9a86fd1a30c82d7d150247ae1a942ea Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 12:24:55 +0000 Subject: [PATCH] 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 --- .../src/protocol-live-metadata-types.ts | 66 ++++++++++++++++++ .../src/protocol-save-conflict.ts | 68 +++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 49 +++++-------- 3 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol-live-metadata-types.ts create mode 100644 packages/metadata-protocol/src/protocol-save-conflict.ts diff --git a/packages/metadata-protocol/src/protocol-live-metadata-types.ts b/packages/metadata-protocol/src/protocol-live-metadata-types.ts new file mode 100644 index 00000000000..d8a6cd4ceee --- /dev/null +++ b/packages/metadata-protocol/src/protocol-live-metadata-types.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 24] The live metadata-type listing of + * `ObjectStackProtocolImplementation` (`listLiveMetadataTypes`), moved out + * of `protocol.ts` whole: the types the engine's SchemaRegistry holds, plus + * the types the `metadata` service reports, de-duplicated — the one answer to + * "which types exist here" that `getMetaTypes` and every other caller share. + * + * A MOVE, not a redesign. The function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited: the same two sources in the + * same order, the same tolerance of an absent `metadata` service, and the + * same de-duplication. + * + * ## How the moved body reaches the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps the method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link LiveMetadataTypesHost} members it reads, plus a `.call`. Both + * are handed over as getters, so the body reads the protocol's own `engine` + * and its constructor-supplied `getServicesRegistry` field at call time, as + * `protocol-metadata-service-read.ts` hands that field over. Every other + * reader of the method keeps calling the class method. + * + * The body reads none of `protocol.ts`'s module-level declarations and + * imports nothing but the class's type. Nothing re-exports this module, so + * the published surface does not move. + */ + +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the function below reads — + * every one of them, and no other; the function's `this` is the `Pick` of the + * members it reads. Each is typed AS the class member (an indexed access on + * the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface LiveMetadataTypesHost { + readonly engine: ObjectStackProtocolImplementation['engine']; + readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry']; +} + +/** The body of `ObjectStackProtocolImplementation#listLiveMetadataTypes`; the method's docblock there is its contract. */ +export async function listLiveMetadataTypes( + this: Pick, +): Promise { + const schemaTypes = this.engine.registry.getRegisteredTypes(); + + // Also include types from MetadataService (runtime-registered: agent, tool, etc.) + let runtimeTypes: string[] = []; + try { + const services = this.getServicesRegistry?.(); + const metadataService = services?.get('metadata'); + if (metadataService && typeof metadataService.getRegisteredTypes === 'function') { + runtimeTypes = await metadataService.getRegisteredTypes(); + } + } catch { + // MetadataService not available + } + + return Array.from(new Set([...schemaTypes, ...runtimeTypes])); +} diff --git a/packages/metadata-protocol/src/protocol-save-conflict.ts b/packages/metadata-protocol/src/protocol-save-conflict.ts new file mode 100644 index 00000000000..488de2b7859 --- /dev/null +++ b/packages/metadata-protocol/src/protocol-save-conflict.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#25467 cut 24] The save door's answer to an optimistic-save conflict, from + * `ObjectStackProtocolImplementation` (`saveConflict`), moved out of + * `protocol.ts` whole: the 409 for a `ConflictError` — the repository's race, + * or a caller's token the keyed comparison refused — built by the one + * conflict builder, plus its decision-audit row (#21207, #23406). + * + * A MOVE, not a redesign. The function below is the body of the + * `ObjectStackProtocolImplementation` method of the same name as it stood, + * re-indented one level and otherwise unedited: the same refusal text and + * first-write pin, the same audit row, in the same order. + * + * ## How the moved body reaches the class + * + * The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts` + * header carries the full reasoning), as `protocol-batch-data.ts` applies it. + * The class keeps the method — the published `dist/*.d.ts` lists its name, + * docblock and declared signature — and its body is one literal of exactly + * the {@link SaveConflictHost} members it reads, each handed over as a + * closure, plus a `.call`. Every other reader of the method keeps calling + * the class method. + * + * What the body reads from elsewhere, it imports from where `protocol.ts` + * imported it. Nothing re-exports this module, so the published surface does + * not move. + */ + +import type { ConflictError } from '@objectstack/metadata-core'; +import type { ObjectStackProtocolImplementation } from './protocol.js'; + +/** + * The `ObjectStackProtocolImplementation` members the function below reads — + * every one of them, and no other; the function's `this` is the `Pick` of the + * members it reads. Each is typed AS the class member (an indexed access on + * the class), so a change to a member's signature on the class is a change + * here too, and naming a member the class does not have is a compile error. + */ +export interface SaveConflictHost { + readonly metadataConflictRefusal: ObjectStackProtocolImplementation['metadataConflictRefusal']; + readonly recordOptimisticConflictAudit: ObjectStackProtocolImplementation['recordOptimisticConflictAudit']; +} + +/** The body of `ObjectStackProtocolImplementation#saveConflict`; the method's docblock there is its contract. */ +export async function saveConflict( + this: Pick, + err: ConflictError, + request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' }, + writeSource: string, +): Promise { + const conflict = await this.metadataConflictRefusal( + err, + `${request.type}/${request.name}`, + `${request.type}/${request.name} has been modified since you loaded it.`, + request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined, + ); + await this.recordOptimisticConflictAudit({ + type: request.type, + name: request.name, + operation: 'save', + ...(request.actor ? { actor: request.actor } : {}), + source: writeSource, + expectedParent: err.expectedParent, + actualHead: err.actualHead, + }); + return conflict; +} diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index bb8d3788603..3b4476a1f96 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -413,6 +413,10 @@ import { reportSealedOverlayRows } from './protocol-sealed-overlay-report.js'; import { expandUnderOwnName, resolveRowlessExpandedView } from './protocol-view-expansion-names.js'; // [#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. import { convertStoredItem, convertStoredItemDetailed } from './protocol-stored-item-conversion.js'; +// [#25467 cut 24] The live metadata-type listing: the SchemaRegistry's types and the `metadata` service's, de-duplicated. +import { listLiveMetadataTypes } from './protocol-live-metadata-types.js'; +// [#25467 cut 24] The save door's 409 for a `ConflictError`, with its decision-audit row. +import { saveConflict } from './protocol-save-conflict.js'; /** * Canonical Zod schema per metadata type lives in @@ -8492,21 +8496,14 @@ export class ObjectStackProtocolImplementation implements * One accessor, no second vocabulary. */ private async listLiveMetadataTypes(): Promise { - const schemaTypes = this.engine.registry.getRegisteredTypes(); - - // Also include types from MetadataService (runtime-registered: agent, tool, etc.) - let runtimeTypes: string[] = []; - try { - const services = this.getServicesRegistry?.(); - const metadataService = services?.get('metadata'); - if (metadataService && typeof metadataService.getRegisteredTypes === 'function') { - runtimeTypes = await metadataService.getRegisteredTypes(); - } - } catch { - // MetadataService not available - } - - return Array.from(new Set([...schemaTypes, ...runtimeTypes])); + // [#25467 cut 24] The body moved to `protocol-live-metadata-types.ts` (see `LiveMetadataTypesHost`). + const readEngine = () => this.engine; + const readGetServicesRegistry = () => this.getServicesRegistry; + const host: ThisParameterType = { + get engine() { return readEngine(); }, + get getServicesRegistry() { return readGetServicesRegistry(); }, + }; + return listLiveMetadataTypes.call(host); } async getMetaTypes() { @@ -21471,22 +21468,12 @@ export class ObjectStackProtocolImplementation implements request: { type: string; name: string; actor?: string; parentVersion?: string | null; mode?: 'draft' | 'publish' }, writeSource: string, ): Promise { - const conflict = await this.metadataConflictRefusal( - err, - `${request.type}/${request.name}`, - `${request.type}/${request.name} has been modified since you loaded it.`, - request.parentVersion === null ? (request.mode === 'draft' ? 'draft' : 'active') : undefined, - ); - await this.recordOptimisticConflictAudit({ - type: request.type, - name: request.name, - operation: 'save', - ...(request.actor ? { actor: request.actor } : {}), - source: writeSource, - expectedParent: err.expectedParent, - actualHead: err.actualHead, - }); - return conflict; + // [#25467 cut 24] The body moved to `protocol-save-conflict.ts` (see `SaveConflictHost`). + const host: ThisParameterType = { + metadataConflictRefusal: (...a) => this.metadataConflictRefusal(...a), + recordOptimisticConflictAudit: (...a) => this.recordOptimisticConflictAudit(...a), + }; + return saveConflict.call(host, err, request, writeSource); } /**