RegisterDynamicTypeDef fall back

This commit is contained in:
2026-08-20 00:32:01 +03:00
parent 3e608ef1d1
commit f14dc633ae
6 changed files with 167 additions and 4 deletions
@@ -1393,7 +1393,11 @@ partial class EpConnection
{ {
var typeDefs = LocalTypeDef.GetDependencies(localTypeDef, Instance.Warehouse); var typeDefs = LocalTypeDef.GetDependencies(localTypeDef, Instance.Warehouse);
// Send // Send
SendReply(EpPacketReply.Completed, callback, typeDefs.Select(x => x.Compose(this)).ToArray()); // Cast reference-type arrays to object so C# does not expand
// them into SendReply's params object[] argument. The array is
// one protocol value, not N reply arguments.
SendReply(EpPacketReply.Completed, callback,
(object)typeDefs.Select(x => x.Compose(this)).ToArray());
} }
else else
{ {
@@ -1582,7 +1586,10 @@ partial class EpConnection
var list = children var list = children
.Where(x => IsOperationAllowed(x, null, ActionType.Attach)) .Where(x => IsOperationAllowed(x, null, ActionType.Attach))
.ToArray(); .ToArray();
SendReply(EpPacketReply.Completed, callback, list); // IResource[] is covariant with object[]; without this
// cast a one-child Query becomes one bare resource on the
// wire instead of a ResourceList.
SendReply(EpPacketReply.Completed, callback, (object)list);
}).Error(e => }).Error(e =>
{ {
SendError(e.Type, callback, (ushort)e.Code, e.Message); SendError(e.Type, callback, (ushort)e.Code, e.Message);
@@ -3098,7 +3105,14 @@ partial class EpConnection
var defs = new List<RemoteTypeDef>(); var defs = new List<RemoteTypeDef>();
foreach (var def in (byte[][])result) var typeDefinitions = result switch
{
byte[][] values => values,
object[] values => values.Cast<byte[]>().ToArray(),
_ => throw new InvalidCastException($"Expected an array of type definitions, received {result?.GetType().FullName ?? "null"}.")
};
foreach (var def in typeDefinitions)
{ {
var od = new RemoteTypeDef(); var od = new RemoteTypeDef();
await RemoteTypeDef.Parse(od, _remoteDomain, def, this, null); await RemoteTypeDef.Parse(od, _remoteDomain, def, this, null);
@@ -3825,7 +3839,14 @@ partial class EpConnection
SendRequest(EpPacketRequest.Query, path) SendRequest(EpPacketRequest.Query, path)
.Then(result => .Then(result =>
{ {
reply.Trigger((IResource[])result); var resources = result switch
{
IResource[] values => values,
object[] values => values.Cast<IResource>().ToArray(),
_ => throw new InvalidCastException($"Expected a resource array, received {result?.GetType().FullName ?? "null"}.")
};
reply.Trigger(resources);
}).Error(ex => reply.TriggerError(ex)); }).Error(ex => reply.TriggerError(ex));
return reply; return reply;
+11
View File
@@ -1375,7 +1375,18 @@ public class Warehouse
} }
if (_remoteTypeDefs[domain].ContainsKey(typeDef.Id)) if (_remoteTypeDefs[domain].ContainsKey(typeDef.Id))
{
// Remote definitions are scoped by domain and survive an
// EpConnection reconnect in the Warehouse cache. A freshly
// parsed definition for that same remote id must inherit the
// Warehouse-local id of the cached definition. Leaving it at
// zero makes Instance.RegisterDynamicTypeDef fall back to the
// producer's wire id, which can collide with an unrelated
// local CLR definition.
var existing = _remoteTypeDefs[domain][typeDef.Id];
typeDef.LocalTypeDefId = existing.LocalTypeDefId;
return false; return false;
}
// @TODO: Try to find a proxy type for the remote type def, if not found, create a new proxy type and register it in the warehouse. // @TODO: Try to find a proxy type for the remote type def, if not found, create a new proxy type and register it in the warehouse.
_remoteTypeDefs[domain][typeDef.Id] = typeDef; _remoteTypeDefs[domain][typeDef.Id] = typeDef;
@@ -0,0 +1,26 @@
{
"fixtureVersion": 1,
"protocolVersion": "3.1",
"producer": "esiur-ts",
"encoding": "indexed-TypeDefInfo",
"payloadBase64": "iaSIolEDAckWCAAIAQgCCAMIBAggCCEIIggjCCQIJcmFCQMIEUkEUHVtcEkTQnVpbGRpbmcuQXV0b21hdGlvbggASRxDb250cm9scyBhIGNpcmN1bGF0aW9uIHB1bXAuSQ9QdW1wIGNvbnRyb2xsZXJJDGVuYWJsZWQ9dHJ1ZUkESFZBQ0kDMy4xiBpREhLJB0kFb3duZXLJDEkKb3BlcmF0aW9ucw==",
"expected": {
"id": 17,
"kind": "Resource",
"version": 3,
"name": "Building.Automation.Pump",
"namespace": "Building.Automation",
"usage": "Controls a circulation pump.",
"description": "Pump controller",
"example": "enabled=true",
"category": "HVAC",
"since": "3.1",
"annotations": {
"owner": "operations"
},
"properties": [],
"functions": [],
"events": [],
"constants": []
}
}
+4
View File
@@ -22,6 +22,10 @@
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="ConformanceFixtures\**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<!-- Reference Esiur both as a runtime assembly and as the source generator (analyzer), <!-- Reference Esiur both as a runtime assembly and as the source generator (analyzer),
mirroring Tests/Features/Functional so [Resource]/[Export] test types get generated code. --> mirroring Tests/Features/Functional so [Resource]/[Export] test types get generated code. -->
<ItemGroup> <ItemGroup>
@@ -0,0 +1,66 @@
using System.Text.Json;
using Esiur.Data;
using Esiur.Data.Types;
using Esiur.Resource;
namespace Esiur.Tests.Unit;
public class TypeDefConformanceFixtureTests
{
[Fact]
public void DotnetEncoder_MatchesCanonicalTypeScriptFixture()
{
var root = ReadFixture();
var info = new TypeDefInfo
{
Version = 3,
Id = 17,
Name = "Pump",
Namespace = "Building.Automation",
Kind = TypeDefKind.Resource,
Usage = "Controls a circulation pump.",
Description = "Pump controller",
Example = "enabled=true",
Category = "HVAC",
Since = "3.1",
Annotations = new Map<string, string> { ["owner"] = "operations" },
};
var encoded = Codec.Compose(info, Warehouse.Default, null);
Assert.Equal(root.GetProperty("payloadBase64").GetString(), Convert.ToBase64String(encoded));
}
[Fact]
public void TypeScriptIndexedTypeDefInfoFixture_DecodesWithExpectedSemantics()
{
var root = ReadFixture();
var expected = root.GetProperty("expected");
var payload = Convert.FromBase64String(root.GetProperty("payloadBase64").GetString()!);
var (size, info) = Codec.ParseIndexedType<TypeDefInfo>(payload, 0, Warehouse.Default);
Assert.Equal((uint)payload.Length, size);
Assert.Equal(expected.GetProperty("id").GetUInt64(), info.Id);
Assert.Equal(expected.GetProperty("version").GetInt32(), info.Version);
Assert.Equal("Pump", info.Name);
Assert.Equal(expected.GetProperty("namespace").GetString(), info.Namespace);
Assert.Equal(expected.GetProperty("usage").GetString(), info.Usage);
Assert.Equal(expected.GetProperty("description").GetString(), info.Description);
Assert.Equal(expected.GetProperty("example").GetString(), info.Example);
Assert.Equal(expected.GetProperty("category").GetString(), info.Category);
Assert.Equal(expected.GetProperty("since").GetString(), info.Since);
Assert.Equal(
expected.GetProperty("annotations").GetProperty("owner").GetString(),
info.Annotations!["owner"]);
}
private static JsonElement ReadFixture()
{
var location = Path.Combine(
AppContext.BaseDirectory,
"ConformanceFixtures",
"typedef-info-v3-flat.json");
using var document = JsonDocument.Parse(File.ReadAllText(location));
return document.RootElement.Clone();
}
}
+35
View File
@@ -1,4 +1,5 @@
using Esiur.Core; using Esiur.Core;
using Esiur.Data.Types;
using Esiur.Resource; using Esiur.Resource;
using Esiur.Stores; using Esiur.Stores;
@@ -6,6 +7,25 @@ namespace Esiur.Tests.Unit;
public sealed class WarehouseLifecycleTests public sealed class WarehouseLifecycleTests
{ {
[Fact]
public void ReparsedRemoteTypeDef_ReusesWarehouseLocalId()
{
var warehouse = new Warehouse();
_ = new LocalTypeDef(typeof(LocalCollisionMarker), warehouse);
var first = new TestRemoteTypeDef(1, "Remote.ReportingService");
Assert.True(warehouse.TryRegisterRemoteTypeDef("flow.example", first));
Assert.NotEqual(0u, first.LocalTypeDefId);
var reparsed = new TestRemoteTypeDef(1, "Remote.ReportingService");
Assert.False(warehouse.TryRegisterRemoteTypeDef("flow.example", reparsed));
Assert.Equal(first.LocalTypeDefId, reparsed.LocalTypeDefId);
// Dynamic EpResource construction registers the definition again.
// This must use the reused local id, not the colliding wire id 1.
warehouse.RegisterDynamicTypeDef(reparsed);
}
[Fact] [Fact]
public async Task Close_AllowsWarehouseToBeOpenedAgain() public async Task Close_AllowsWarehouseToBeOpenedAgain()
{ {
@@ -176,6 +196,21 @@ public sealed class WarehouseLifecycleTests
private static async Task<bool> Observe(AsyncReply<bool> reply) => await reply; private static async Task<bool> Observe(AsyncReply<bool> reply) => await reply;
private enum LocalCollisionMarker
{
Value,
}
private sealed class TestRemoteTypeDef : RemoteTypeDef
{
public TestRemoteTypeDef(ulong id, string name)
{
_typeId = id;
_typeName = name;
_typeDefKind = TypeDefKind.Resource;
}
}
private sealed class ControlledLifecycleResource : IResource private sealed class ControlledLifecycleResource : IResource
{ {
private readonly AsyncReply<bool>? terminateReply; private readonly AsyncReply<bool>? terminateReply;