mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-09-08 10:10:49 +00:00
RegisterDynamicTypeDef fall back
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user