Carry editable field authority in CultMesh bindings
This commit is contained in:
parent
21163d83fa
commit
4f6e95bb3d
5 changed files with 257 additions and 8 deletions
|
|
@ -300,6 +300,13 @@ export interface CultMeshStateBindingDescriptor {
|
|||
readonly sourceId?: string;
|
||||
readonly schemaId?: string;
|
||||
readonly routeHint: CultMeshRouteHint;
|
||||
readonly bindingName?: string;
|
||||
readonly documentId?: string;
|
||||
readonly fieldPath?: string;
|
||||
readonly valueKind?: "string" | "number" | "boolean" | "choice" | "string-list";
|
||||
readonly accessMode?: "read" | "write" | "read-write" | "local-draft";
|
||||
readonly authority?: string;
|
||||
readonly writeCommand?: string;
|
||||
}
|
||||
|
||||
export interface CultMeshStateBindingRecord {
|
||||
|
|
@ -309,6 +316,13 @@ export interface CultMeshStateBindingRecord {
|
|||
readonly schemaId: string;
|
||||
readonly routeKind: string;
|
||||
readonly routeDescription: string;
|
||||
readonly bindingName: string;
|
||||
readonly documentId: string;
|
||||
readonly fieldPath: string;
|
||||
readonly valueKind: string;
|
||||
readonly accessMode: string;
|
||||
readonly authority: string;
|
||||
readonly writeCommand: string;
|
||||
}
|
||||
|
||||
export interface CultMeshOperationBindingDescriptor {
|
||||
|
|
@ -3351,6 +3365,15 @@ export function cultMeshDescribeStateRefResolver(
|
|||
};
|
||||
}
|
||||
|
||||
function stateBindingMetadata(
|
||||
options: Pick<CultMeshStateBindingDescriptor,
|
||||
"bindingName" | "documentId" | "fieldPath" | "valueKind" | "accessMode" | "authority" | "writeCommand">,
|
||||
): Partial<CultMeshStateBindingDescriptor> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([, value]) => value !== undefined),
|
||||
) as Partial<CultMeshStateBindingDescriptor>;
|
||||
}
|
||||
|
||||
export function cultMeshStateBinding(
|
||||
targetProp: string,
|
||||
pointer:
|
||||
|
|
@ -3364,6 +3387,13 @@ export function cultMeshStateBinding(
|
|||
sourceId?: string;
|
||||
schemaId?: string;
|
||||
routeHint?: CultMeshRouteHint;
|
||||
bindingName?: string;
|
||||
documentId?: string;
|
||||
fieldPath?: string;
|
||||
valueKind?: CultMeshStateBindingDescriptor["valueKind"];
|
||||
accessMode?: CultMeshStateBindingDescriptor["accessMode"];
|
||||
authority?: string;
|
||||
writeCommand?: string;
|
||||
} = {},
|
||||
): CultMeshStateBindingDescriptor {
|
||||
requireNonEmpty(targetProp, "targetProp");
|
||||
|
|
@ -3375,6 +3405,7 @@ export function cultMeshStateBinding(
|
|||
sourceId: options.sourceId,
|
||||
schemaId: options.schemaId,
|
||||
routeHint: options.routeHint ?? cultMeshRouteHint(),
|
||||
...stateBindingMetadata(options),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3386,6 +3417,7 @@ export function cultMeshStateBinding(
|
|||
sourceId: options.sourceId ?? source?.sourceId,
|
||||
schemaId: options.schemaId ?? source?.schemaId,
|
||||
routeHint: options.routeHint ?? pointer.routeHint ?? cultMeshRouteHint(),
|
||||
...stateBindingMetadata(options),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3399,6 +3431,13 @@ export function cultMeshStateBindingRecord(
|
|||
schemaId?: string,
|
||||
routeKind?: string,
|
||||
routeDescription?: string,
|
||||
bindingName?: string,
|
||||
documentId?: string,
|
||||
fieldPath?: string,
|
||||
valueKind?: string,
|
||||
accessMode?: string,
|
||||
authority?: string,
|
||||
writeCommand?: string,
|
||||
): CultMeshStateBindingRecord;
|
||||
export function cultMeshStateBindingRecord(
|
||||
bindingOrTargetProp?: Partial<CultMeshStateBindingDescriptor> | string,
|
||||
|
|
@ -3407,6 +3446,13 @@ export function cultMeshStateBindingRecord(
|
|||
schemaId?: string,
|
||||
routeKind?: string,
|
||||
routeDescription?: string,
|
||||
bindingName?: string,
|
||||
documentId?: string,
|
||||
fieldPath?: string,
|
||||
valueKind?: string,
|
||||
accessMode?: string,
|
||||
authority?: string,
|
||||
writeCommand?: string,
|
||||
): CultMeshStateBindingRecord {
|
||||
if (typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined) {
|
||||
const binding = bindingOrTargetProp;
|
||||
|
|
@ -3418,6 +3464,13 @@ export function cultMeshStateBindingRecord(
|
|||
schemaId: binding?.schemaId ?? "",
|
||||
routeKind: route.kind,
|
||||
routeDescription: route.description,
|
||||
bindingName: binding?.bindingName ?? "",
|
||||
documentId: binding?.documentId ?? "",
|
||||
fieldPath: binding?.fieldPath ?? "",
|
||||
valueKind: binding?.valueKind ?? "",
|
||||
accessMode: binding?.accessMode ?? "",
|
||||
authority: binding?.authority ?? "",
|
||||
writeCommand: binding?.writeCommand ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3428,6 +3481,13 @@ export function cultMeshStateBindingRecord(
|
|||
schemaId: schemaId ?? "",
|
||||
routeKind: routeKind ?? "",
|
||||
routeDescription: routeDescription ?? "",
|
||||
bindingName: bindingName ?? "",
|
||||
documentId: documentId ?? "",
|
||||
fieldPath: fieldPath ?? "",
|
||||
valueKind: valueKind ?? "",
|
||||
accessMode: accessMode ?? "",
|
||||
authority: authority ?? "",
|
||||
writeCommand: writeCommand ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3451,6 +3511,15 @@ export function cultMeshStateBindingFromRecord(
|
|||
},
|
||||
options.fallbackRouteHint,
|
||||
),
|
||||
...stateBindingMetadata({
|
||||
bindingName: record.bindingName || undefined,
|
||||
documentId: record.documentId || undefined,
|
||||
fieldPath: record.fieldPath || undefined,
|
||||
valueKind: (record.valueKind || undefined) as CultMeshStateBindingDescriptor["valueKind"],
|
||||
accessMode: (record.accessMode || undefined) as CultMeshStateBindingDescriptor["accessMode"],
|
||||
authority: record.authority || undefined,
|
||||
writeCommand: record.writeCommand || undefined,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -5278,6 +5347,13 @@ export class CultMesh {
|
|||
sourceId?: string;
|
||||
schemaId?: string;
|
||||
routeHint?: CultMeshRouteHint;
|
||||
bindingName?: string;
|
||||
documentId?: string;
|
||||
fieldPath?: string;
|
||||
valueKind?: CultMeshStateBindingDescriptor["valueKind"];
|
||||
accessMode?: CultMeshStateBindingDescriptor["accessMode"];
|
||||
authority?: string;
|
||||
writeCommand?: string;
|
||||
} = {},
|
||||
): CultMeshStateBindingDescriptor {
|
||||
return cultMeshStateBinding(targetProp, pointer, options);
|
||||
|
|
@ -5293,6 +5369,13 @@ export class CultMesh {
|
|||
schemaId?: string,
|
||||
routeKind?: string,
|
||||
routeDescription?: string,
|
||||
bindingName?: string,
|
||||
documentId?: string,
|
||||
fieldPath?: string,
|
||||
valueKind?: string,
|
||||
accessMode?: string,
|
||||
authority?: string,
|
||||
writeCommand?: string,
|
||||
): CultMeshStateBindingRecord;
|
||||
public static stateBindingRecord(
|
||||
bindingOrTargetProp?: Partial<CultMeshStateBindingDescriptor> | string,
|
||||
|
|
@ -5301,6 +5384,13 @@ export class CultMesh {
|
|||
schemaId?: string,
|
||||
routeKind?: string,
|
||||
routeDescription?: string,
|
||||
bindingName?: string,
|
||||
documentId?: string,
|
||||
fieldPath?: string,
|
||||
valueKind?: string,
|
||||
accessMode?: string,
|
||||
authority?: string,
|
||||
writeCommand?: string,
|
||||
): CultMeshStateBindingRecord {
|
||||
return typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined
|
||||
? cultMeshStateBindingRecord(bindingOrTargetProp)
|
||||
|
|
@ -5311,6 +5401,13 @@ export class CultMesh {
|
|||
schemaId,
|
||||
routeKind,
|
||||
routeDescription,
|
||||
bindingName,
|
||||
documentId,
|
||||
fieldPath,
|
||||
valueKind,
|
||||
accessMode,
|
||||
authority,
|
||||
writeCommand,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2147,6 +2147,13 @@ test("CultMesh TS flattens and rehydrates UI state binding records", () => {
|
|||
schemaId: "gamecult.aetheria.daemon_frame.v1",
|
||||
routeKind: "shared-memory",
|
||||
routeDescription: "co-located frame slab",
|
||||
bindingName: "",
|
||||
documentId: "",
|
||||
fieldPath: "",
|
||||
valueKind: "",
|
||||
accessMode: "",
|
||||
authority: "",
|
||||
writeCommand: "",
|
||||
});
|
||||
|
||||
assert.deepEqual(CultMesh.stateBindingFromRecord({
|
||||
|
|
@ -2168,6 +2175,26 @@ test("CultMesh TS flattens and rehydrates UI state binding records", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("CultMesh TS preserves editable binding identity and authority through transport records", () => {
|
||||
const binding = CultMesh.stateBinding("value", "ghostlight.session-zero.composer", {
|
||||
sourceId: "session-zero:draft-7",
|
||||
schemaId: "session_zero.v1",
|
||||
routeHint: CultMesh.routeHint("network", "Ghostlight provider"),
|
||||
bindingName: "composer.message",
|
||||
documentId: "session-zero:draft-7",
|
||||
fieldPath: "channels.private.messages.pending",
|
||||
valueKind: "string",
|
||||
accessMode: "local-draft",
|
||||
authority: "SessionZeroKernel",
|
||||
writeCommand: "edit.value",
|
||||
});
|
||||
const record = CultMesh.stateBindingRecord(binding);
|
||||
assert.equal(record.bindingName, "composer.message");
|
||||
assert.equal(record.fieldPath, "channels.private.messages.pending");
|
||||
assert.equal(record.accessMode, "local-draft");
|
||||
assert.deepEqual(CultMesh.stateBindingFromRecord(record), binding);
|
||||
});
|
||||
|
||||
test("CultMesh TS describes UI command bindings from typed operations", async () => {
|
||||
const operation = CultMesh.operation<
|
||||
{ actor: string },
|
||||
|
|
|
|||
|
|
@ -1117,9 +1117,18 @@ namespace GameCult.Mesh
|
|||
string pointerId,
|
||||
string? sourceId = null,
|
||||
string? schemaId = null,
|
||||
CultMeshRouteHint? routeHint = null)
|
||||
CultMeshRouteHint? routeHint = null,
|
||||
string? bindingName = null,
|
||||
string? documentId = null,
|
||||
string? fieldPath = null,
|
||||
string? valueKind = null,
|
||||
string? accessMode = null,
|
||||
string? authority = null,
|
||||
string? writeCommand = null)
|
||||
{
|
||||
return new CultMeshStateBindingDescriptor(targetProp, pointerId, sourceId, schemaId, routeHint);
|
||||
return new CultMeshStateBindingDescriptor(
|
||||
targetProp, pointerId, sourceId, schemaId, routeHint,
|
||||
bindingName, documentId, fieldPath, valueKind, accessMode, authority, writeCommand);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1139,7 +1148,14 @@ namespace GameCult.Mesh
|
|||
string? sourceId = null,
|
||||
string? schemaId = null,
|
||||
string? routeKind = null,
|
||||
string? routeDescription = null)
|
||||
string? routeDescription = null,
|
||||
string? bindingName = null,
|
||||
string? documentId = null,
|
||||
string? fieldPath = null,
|
||||
string? valueKind = null,
|
||||
string? accessMode = null,
|
||||
string? authority = null,
|
||||
string? writeCommand = null)
|
||||
{
|
||||
return new CultMeshStateBindingRecord(
|
||||
targetProp,
|
||||
|
|
@ -1147,7 +1163,14 @@ namespace GameCult.Mesh
|
|||
sourceId,
|
||||
schemaId,
|
||||
routeKind,
|
||||
routeDescription);
|
||||
routeDescription,
|
||||
bindingName,
|
||||
documentId,
|
||||
fieldPath,
|
||||
valueKind,
|
||||
accessMode,
|
||||
authority,
|
||||
writeCommand);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -3329,13 +3329,27 @@ namespace GameCult.Mesh
|
|||
string pointerId,
|
||||
string? sourceId = null,
|
||||
string? schemaId = null,
|
||||
CultMeshRouteHint? routeHint = null)
|
||||
CultMeshRouteHint? routeHint = null,
|
||||
string? bindingName = null,
|
||||
string? documentId = null,
|
||||
string? fieldPath = null,
|
||||
string? valueKind = null,
|
||||
string? accessMode = null,
|
||||
string? authority = null,
|
||||
string? writeCommand = null)
|
||||
{
|
||||
TargetProp = RequireNonEmpty(targetProp, nameof(targetProp));
|
||||
PointerId = RequireNonEmpty(pointerId, nameof(pointerId));
|
||||
SourceId = sourceId;
|
||||
SchemaId = schemaId;
|
||||
RouteHint = routeHint ?? CultMeshRouteHint.Automatic;
|
||||
BindingName = bindingName ?? "";
|
||||
DocumentId = documentId ?? "";
|
||||
FieldPath = fieldPath ?? "";
|
||||
ValueKind = valueKind ?? "";
|
||||
AccessMode = accessMode ?? "";
|
||||
Authority = authority ?? "";
|
||||
WriteCommand = writeCommand ?? "";
|
||||
}
|
||||
|
||||
/// <summary>Gets the component property receiving the resolved state value.</summary>
|
||||
|
|
@ -3353,6 +3367,27 @@ namespace GameCult.Mesh
|
|||
/// <summary>Gets the preferred route for resolving and watching the binding.</summary>
|
||||
public CultMeshRouteHint RouteHint { get; }
|
||||
|
||||
/// <summary>Gets the stable renderer-facing name used to capture this binding in an operation.</summary>
|
||||
public string BindingName { get; }
|
||||
|
||||
/// <summary>Gets the optional identity of the provider-owned backing document.</summary>
|
||||
public string DocumentId { get; }
|
||||
|
||||
/// <summary>Gets the optional typed field path inside the backing document.</summary>
|
||||
public string FieldPath { get; }
|
||||
|
||||
/// <summary>Gets the optional portable value kind.</summary>
|
||||
public string ValueKind { get; }
|
||||
|
||||
/// <summary>Gets the optional read, write, read-write, or local-draft access mode.</summary>
|
||||
public string AccessMode { get; }
|
||||
|
||||
/// <summary>Gets the authority responsible for accepting writes to this binding.</summary>
|
||||
public string Authority { get; }
|
||||
|
||||
/// <summary>Gets the optional typed operation id used for direct writes.</summary>
|
||||
public string WriteCommand { get; }
|
||||
|
||||
/// <summary>Creates a binding from an existing state pointer.</summary>
|
||||
public static CultMeshStateBindingDescriptor FromPointer<TValue>(
|
||||
string targetProp,
|
||||
|
|
@ -3390,7 +3425,14 @@ namespace GameCult.Mesh
|
|||
string? sourceId = null,
|
||||
string? schemaId = null,
|
||||
string? routeKind = null,
|
||||
string? routeDescription = null)
|
||||
string? routeDescription = null,
|
||||
string? bindingName = null,
|
||||
string? documentId = null,
|
||||
string? fieldPath = null,
|
||||
string? valueKind = null,
|
||||
string? accessMode = null,
|
||||
string? authority = null,
|
||||
string? writeCommand = null)
|
||||
{
|
||||
TargetProp = string.IsNullOrWhiteSpace(targetProp) ? "value" : targetProp!;
|
||||
PointerId = pointerId ?? "";
|
||||
|
|
@ -3398,6 +3440,13 @@ namespace GameCult.Mesh
|
|||
SchemaId = schemaId ?? "";
|
||||
RouteKind = routeKind ?? "";
|
||||
RouteDescription = routeDescription ?? "";
|
||||
BindingName = bindingName ?? "";
|
||||
DocumentId = documentId ?? "";
|
||||
FieldPath = fieldPath ?? "";
|
||||
ValueKind = valueKind ?? "";
|
||||
AccessMode = accessMode ?? "";
|
||||
Authority = authority ?? "";
|
||||
WriteCommand = writeCommand ?? "";
|
||||
}
|
||||
|
||||
/// <summary>Gets the component property receiving the resolved state value.</summary>
|
||||
|
|
@ -3418,6 +3467,14 @@ namespace GameCult.Mesh
|
|||
/// <summary>Gets the flattened route description.</summary>
|
||||
[Key(5)] public string RouteDescription { get; }
|
||||
|
||||
[Key(6)] public string BindingName { get; }
|
||||
[Key(7)] public string DocumentId { get; }
|
||||
[Key(8)] public string FieldPath { get; }
|
||||
[Key(9)] public string ValueKind { get; }
|
||||
[Key(10)] public string AccessMode { get; }
|
||||
[Key(11)] public string Authority { get; }
|
||||
[Key(12)] public string WriteCommand { get; }
|
||||
|
||||
/// <summary>Creates transport-friendly fields from a state binding descriptor.</summary>
|
||||
public static CultMeshStateBindingRecord FromBinding(CultMeshStateBindingDescriptor? binding)
|
||||
{
|
||||
|
|
@ -3428,7 +3485,14 @@ namespace GameCult.Mesh
|
|||
binding?.SourceId,
|
||||
binding?.SchemaId,
|
||||
route.Kind,
|
||||
route.Description);
|
||||
route.Description,
|
||||
binding?.BindingName,
|
||||
binding?.DocumentId,
|
||||
binding?.FieldPath,
|
||||
binding?.ValueKind,
|
||||
binding?.AccessMode,
|
||||
binding?.Authority,
|
||||
binding?.WriteCommand);
|
||||
}
|
||||
|
||||
/// <summary>Rehydrates this record as a state binding descriptor.</summary>
|
||||
|
|
@ -3450,7 +3514,14 @@ namespace GameCult.Mesh
|
|||
pointerId,
|
||||
SourceId,
|
||||
SchemaId,
|
||||
route);
|
||||
route,
|
||||
BindingName,
|
||||
DocumentId,
|
||||
FieldPath,
|
||||
ValueKind,
|
||||
AccessMode,
|
||||
Authority,
|
||||
WriteCommand);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -298,6 +298,37 @@ public sealed class CultMeshStreamingTests
|
|||
fromTypescript.RouteHint.Description.Should().Be("browser-adjacent cache");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StateBindingRecord_PreservesEditableFieldIdentityAndAuthority()
|
||||
{
|
||||
var binding = CultMesh.StateBinding(
|
||||
"value",
|
||||
"ghostlight.session-zero.composer",
|
||||
sourceId: "session-zero:draft-7",
|
||||
schemaId: "session_zero.v1",
|
||||
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "Ghostlight provider"),
|
||||
bindingName: "composer.message",
|
||||
documentId: "session-zero:draft-7",
|
||||
fieldPath: "channels.private.messages.pending",
|
||||
valueKind: "string",
|
||||
accessMode: "local-draft",
|
||||
authority: "SessionZeroKernel",
|
||||
writeCommand: "edit.value");
|
||||
|
||||
var record = CultMesh.StateBindingRecord(binding);
|
||||
record.BindingName.Should().Be("composer.message");
|
||||
record.FieldPath.Should().Be("channels.private.messages.pending");
|
||||
record.AccessMode.Should().Be("local-draft");
|
||||
var restored = record.ToBinding();
|
||||
restored.BindingName.Should().Be(binding.BindingName);
|
||||
restored.DocumentId.Should().Be(binding.DocumentId);
|
||||
restored.FieldPath.Should().Be(binding.FieldPath);
|
||||
restored.ValueKind.Should().Be(binding.ValueKind);
|
||||
restored.AccessMode.Should().Be(binding.AccessMode);
|
||||
restored.Authority.Should().Be(binding.Authority);
|
||||
restored.WriteCommand.Should().Be(binding.WriteCommand);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OperationBindingDescriptor_BindsUiCommandsToTypedOperations()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue