Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .cursor/rules/interface-negotiate-cpp-ts-parity.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description: Interface negotiate and remote-offer gating — keep C++ and TypeScript gluecode in sync
globs: cpp-lib/**/*,compiler/back-ends/ts-gen/**/*
alwaysApply: false
---

# Interface negotiate — C++ and TypeScript together

Applies to **remote capabilities**, **asnNegotiateInterface**, and **client-side gating** of outbound invokes based on what the peer advertised.

## Paired surfaces (change both in one task)

| C++ (`cpp-lib/`) | TypeScript (`compiler/back-ends/ts-gen/gluecode/`) |
|------------------|------------------------------------------------------|
| `ISnaccRoseSessionSubscription`, `SnaccROSESender`, `SnaccROSEComponent` | `IRoseSessionSubscription`, `IASN1Transport`, `ROSEBase`, `RoseSessionSubscriptionStore` |
| `SetOperationBlockPolicy` / `IsOperationBlocked` | `setOperationBlockPolicy` / `isOperationBlocked` |
| `CompleteIfProcessingShutdown` / `CompleteIfOperationBlocked` on `SendEvent` / `SendInvoke` | `completeIfProcessingShutdown` / `completeIfOperationBlocked` on `handleEvent` / `handleInvoke` and `sendInvoke` |
| `PauseRoseProcessing` / `ResumeRoseProcessing` / `LookUpInterfaceID` on `SnaccROSEBase` | `pauseRoseProcessing` / `resumeRoseProcessing` / `lookUpInterfaceID` on `TSASN1Base` |
| `SnaccROSEBase`, `SnaccModuleCapabilities`, `SnaccRoseOperationLookup` | `TSASN1Base`, `TSModuleCapabilities`, `TSROSEBase` |
| `cpp-lib/tests/` (runtime / capability tests) | `compiler/back-ends/ts-gen/tests/` |

Do **not** land C++-only or TS-only halves of the same behavior. Tests on both sides move together when semantics change.

## Naming

- **Ask the user** before introducing or renaming public API (types, enums, methods, flags).
- Propose 1–2 options with a one-line rationale; **do not implement renames** until they pick one.

## Branching

- Feature work on a ticket branch (e.g. `feature/UCAAS-1486`) in this repo.
- ProCall / `global` pin updates follow after the agreed API is stable — not as a workaround for missing TS or C++ work here.
20 changes: 20 additions & 0 deletions compiler/back-ends/ts-gen/gen-ts-combined.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ void PrintTSRootTypes(FILE* src, Module* mod, const char* szSuffix)

fprintf(src, "export const MODULE_NAME = \"%s\";\n", mod->moduleName);

{
ValueDef* vd;
int iFirstIIDFound = 0;
FOR_EACH_LIST_ELMT(vd, mod->valueDefs)
{
if (vd->value->basicValue->choiceId != BASICVALUE_INTEGER)
continue;
if (vd->value->type->basicType->choiceId != BASICTYPE_MACROTYPE)
continue;
if (vd->value->type->basicType->a.macroType->choiceId != MACROTYPE_ROSOPERATION)
continue;
if (!iFirstIIDFound)
{
iFirstIIDFound = 1;
fprintf(src, "export const MODULE_IID = %d;\n", vd->value->basicValue->a.integer);
}
break;
}
}

if (gMajorInterfaceVersion >= 0)
{
long long lMinorModuleVersion = GetModulePatchVersion(mod->moduleName);
Expand Down
15 changes: 14 additions & 1 deletion compiler/back-ends/ts-gen/gen-ts-rose.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ void SaveTSROSEFilesToOutputDirectory(const int genRoseStubs, const char* szPath
strcat_s(szFileName, _MAX_PATH - 1, "TSROSEBase.ts");
SaveResourceToFile(ETS_ROSE_BASE, szFileName);
}
{
char szFileName[_MAX_PATH] = {0};
strcpy_s(szFileName, _MAX_PATH - 1, szPath);
strcat_s(szFileName, _MAX_PATH - 1, "IRoseSessionSubscription.ts");
SaveResourceToFile(ETS_ROSE_SESSION_SUBSCRIPTION, szFileName);
}
{
char szFileName[_MAX_PATH] = {0};
strcpy_s(szFileName, _MAX_PATH - 1, szPath);
strcat_s(szFileName, _MAX_PATH - 1, "RoseSessionSubscriptionStore.ts");
SaveResourceToFile(ETS_ROSE_SESSION_SUBSCRIPTION_STORE, szFileName);
}
{
char szFileName[_MAX_PATH] = {0};
strcpy_s(szFileName, _MAX_PATH - 1, szPath);
Expand Down Expand Up @@ -498,10 +510,11 @@ void PrintTSROSESetHandler(FILE* src, Module* m)

fprintf(
src,
"\t\tthis.transport.registerOperation(this, handler, OperationIDs.OPID_%s, \"%s\", %s.MODULE_NAME, %lld, %lld, %s);\n",
"\t\tthis.transport.registerOperation(this, handler, OperationIDs.OPID_%s, \"%s\", %s.MODULE_NAME, %s.MODULE_IID, %lld, %lld, %s);\n",
vd->definedName,
vd->definedName,
GetNameSpace(m),
GetNameSpace(m),
llAddedUnix,
llDeprecatedUnix,
bIsEvent ? "true" : "false");
Expand Down
22 changes: 22 additions & 0 deletions compiler/back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Per-session ROSE subscription contract (parity with C++ ISnaccRoseSessionSubscription).
*
* Naming map for future backends:
* C++ clearAllSubscriptions / setSubscribedEvents / isSubscribedEvent
* TS clearAllSubscriptions / setSubscribedEvents / isSubscribedEvent
*
* Server-side transports must override all methods. TSASN1Base defaults call snaccAssertFail.
* When OperationBlockPolicy is BlockUnsupportedOperations and session state is marked,
* gluecode blocks outbound traffic via isOperationBlocked at handleEvent / handleInvoke.
*/
export interface IRoseSessionSubscription {
clearAllSubscriptions(): void;
clearSubscribedEvents(moduleIid: number): void;
clearSupportedInvokes(moduleIid: number): void;
setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void;
addSubscribedEvent(moduleIid: number, eventOpId: number): void;
setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void;
addSupportedInvoke(moduleIid: number, invokeOpId: number): void;
isSubscribedEvent(eventOpId: number): boolean;
isSupportedInvoke(invokeOpId: number): boolean;
}
82 changes: 82 additions & 0 deletions compiler/back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { IRoseSessionSubscription } from "./IRoseSessionSubscription.js";

/*
* In-memory per-session subscription store (reference implementation for TS servers).
* Parity with UCServer ENetCtiSessionSubscriptionStore; reuse the same shape for Kotlin/Swift ports.
*/
export class RoseSessionSubscriptionStore implements IRoseSessionSubscription {
private subscribedEventsByModule = new Map<number, Set<number>>();
private subscribedEventOpIds = new Set<number>();
private supportedInvokesByModule = new Map<number, Set<number>>();
private supportedInvokeOpIds = new Set<number>();

public clearAllSubscriptions(): void {
this.subscribedEventsByModule.clear();
this.subscribedEventOpIds.clear();
this.supportedInvokesByModule.clear();
this.supportedInvokeOpIds.clear();
}

public clearSubscribedEvents(moduleIid: number): void {
this.clearModuleOpIds(this.subscribedEventsByModule, this.subscribedEventOpIds, moduleIid);
}

public clearSupportedInvokes(moduleIid: number): void {
this.clearModuleOpIds(this.supportedInvokesByModule, this.supportedInvokeOpIds, moduleIid);
}

public setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void {
this.replaceModuleOpIds(this.subscribedEventsByModule, this.subscribedEventOpIds, moduleIid, eventOpIds);
}

public addSubscribedEvent(moduleIid: number, eventOpId: number): void {
this.subscribedEventsByModule.set(moduleIid, this.subscribedEventsByModule.get(moduleIid) ?? new Set<number>());
this.subscribedEventsByModule.get(moduleIid)!.add(eventOpId);
this.subscribedEventOpIds.add(eventOpId);
}

public setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void {
this.replaceModuleOpIds(this.supportedInvokesByModule, this.supportedInvokeOpIds, moduleIid, invokeOpIds);
}

public addSupportedInvoke(moduleIid: number, invokeOpId: number): void {
this.supportedInvokesByModule.set(moduleIid, this.supportedInvokesByModule.get(moduleIid) ?? new Set<number>());
this.supportedInvokesByModule.get(moduleIid)!.add(invokeOpId);
this.supportedInvokeOpIds.add(invokeOpId);
}

public isSubscribedEvent(eventOpId: number): boolean {
return this.subscribedEventOpIds.has(eventOpId);
}

public isSupportedInvoke(invokeOpId: number): boolean {
return this.supportedInvokeOpIds.has(invokeOpId);
}

private replaceModuleOpIds(
moduleMap: Map<number, Set<number>>,
flatOpIds: Set<number>,
moduleIid: number,
opIds: readonly number[],
): void {
this.clearModuleOpIds(moduleMap, flatOpIds, moduleIid);
if (opIds.length === 0)
return;

const moduleOpIds = new Set<number>();
for (const opId of opIds) {
moduleOpIds.add(opId);
flatOpIds.add(opId);
}
moduleMap.set(moduleIid, moduleOpIds);
}

private clearModuleOpIds(moduleMap: Map<number, Set<number>>, flatOpIds: Set<number>, moduleIid: number): void {
const previous = moduleMap.get(moduleIid);
if (!previous)
return;
for (const opId of previous)
flatOpIds.delete(opId);
moduleMap.delete(moduleIid);
}
}
Loading
Loading