@deepseek-ai/dsh-typert-generator 0.1.6-alpha.2 → 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/lib/index.js +217 -30
- package/lib/types/analyzer.js +67 -33
- package/lib/types/emitter.js +193 -6
- package/lib/types/model.d.ts +4 -0
- package/package.json +5 -5
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/typert/generator/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 9c207574e9bb576a965ab1a91576a3b5a9db397e
|
|
6
|
+
README.zh.md: 9a6fa9e81d7950efb4d4b76df6921bc5a83b6adf
|
package/README.md
CHANGED
|
@@ -79,12 +79,14 @@ The generator is built on one separation: extraction and emission are decoupled
|
|
|
79
79
|
|
|
80
80
|
### Analysis and faces
|
|
81
81
|
|
|
82
|
-
Host and Client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. A relative import that resolves inside the referencing package is followed through that module's re-exports until a package specifier appears, so package-local forwarding modules keep their original declaration references; a relative import that resolves into another package fails. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded.
|
|
82
|
+
Host and Client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. A relative import that resolves inside the referencing package is followed through that module's re-exports until a package specifier appears, so package-local forwarding modules keep their original declaration references; a relative import that resolves into another package fails. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded. A stream method's return type may be `Iterable<Out>`, `AsyncIterable<Out>`, or the protocol's `RemoteStream<Out, In>`, recognized by symbol and declaring package like the standard-library wrappers; a second type argument other than `never` yields the descriptor's uplink codec, and the generated Client signature returns `RemoteStreamHandle<Out, In>`.
|
|
83
83
|
|
|
84
84
|
### Emission and publication contract
|
|
85
85
|
|
|
86
86
|
`FaceModelEmitter` emits executable JavaScript containing success-cached Zod schema factories and the `TYPERT` contribution, plus a declaration file whose factories return `z.ZodType<SourceType>` through the package's public export; unsupported Zod projections fail. The Host face with Remote methods additionally emits `typert.remote-client.*` projections of Host Remote contracts for the Client. `WorkspaceTypertGenerator` validates each contributor's `package.json`: `./typert` and `./client/typert` (and `./remote` when Remote methods exist) must point at the exact generated files, and the `files` list must include them.
|
|
87
87
|
|
|
88
|
+
[Unary binary results](../protocol/README.md) are recognized after generic type resolution. Their codecs validate the byte-array type and JSON metadata without iterating, copying, or freezing the byte payload; generated Client declarations retain the authored metadata and narrow the byte backing to `ArrayBuffer`.
|
|
89
|
+
|
|
88
90
|
### Catalog projection
|
|
89
91
|
|
|
90
92
|
The runtime type closure indexes exported workspace declarations and referenced framework enums. Vendored declarations stay outside business API discovery; their enums remain available when a public result refers to them.
|
package/README.zh.md
CHANGED
|
@@ -79,12 +79,14 @@ files:
|
|
|
79
79
|
|
|
80
80
|
### 分析与 face
|
|
81
81
|
|
|
82
|
-
Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。解析到本包内部模块的相对导入会沿该模块的重新导出继续追踪,直到出现包说明符,因此包内转发模块保留原始声明引用;解析到其他包的相对导入会失败。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external`
|
|
82
|
+
Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。解析到本包内部模块的相对导入会沿该模块的重新导出继续追踪,直到出现包说明符,因此包内转发模块保留原始声明引用;解析到其他包的相对导入会失败。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。流方法的返回类型可以是 `Iterable<Out>`、`AsyncIterable<Out>` 或协议包的 `RemoteStream<Out, In>`,后者与标准库包装器一样按符号名与声明所在包识别;第二个类型参数不为 `never` 时产出描述符的上行编解码器,生成的 Client 签名则返回 `RemoteStreamHandle<Out, In>`。
|
|
83
83
|
|
|
84
84
|
### 生成与发布约定
|
|
85
85
|
|
|
86
86
|
`FaceModelEmitter` 输出包含只缓存成功结果的 Zod schema factory 与 `TYPERT` 贡献的可执行 JavaScript,以及把 factory 通过包的公开导出标注为返回 `z.ZodType<SourceType>` 的声明文件;不支持的 Zod 投影会失败。含 Remote 方法的 Host face 还会额外为 Client 生成 Host Remote 约定的 `typert.remote-client.*` 投影。`WorkspaceTypertGenerator` 校验每个贡献方的 `package.json`:`./typert` 与 `./client/typert`(存在 Remote 方法时还有 `./remote`)必须指向精确的生成文件,且 `files` 清单必须包含它们。
|
|
87
87
|
|
|
88
|
+
[一元二进制结果](../protocol/README.zh.md)在泛型类型解析后被识别。其 codec 校验字节数组类型与 JSON 元数据,不遍历、复制或冻结字节载荷;生成的 Client 声明保留原有元数据类型,并将字节的底层缓冲区收窄为 `ArrayBuffer`。
|
|
89
|
+
|
|
88
90
|
### 目录投影
|
|
89
91
|
|
|
90
92
|
运行时类型闭包索引导出的 workspace 声明和被引用的框架 enum。Vendor 声明不参与业务 API 发现;公共结果引用其中 enum 时,目录仍提供其定义。
|
package/lib/index.js
CHANGED
|
@@ -774,7 +774,8 @@ var FaceAnalyzer = class {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
const mode = invocation.kind === "direct" ? invocation.mode : void 0;
|
|
777
|
-
const resultType = this.remoteResultType(method, mode);
|
|
777
|
+
const { result: resultType, uplink: uplinkType } = this.remoteResultType(method, mode);
|
|
778
|
+
const uplink = uplinkType === void 0 ? void 0 : { boundary: this.remoteBoundary(uplinkType, `${registration.name}#${binding.namespace}/${exportedMethod}:uplink`, false, "undefined") };
|
|
778
779
|
return {
|
|
779
780
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
780
781
|
service: binding.service,
|
|
@@ -785,8 +786,9 @@ var FaceAnalyzer = class {
|
|
|
785
786
|
invocation: receiver,
|
|
786
787
|
...scope === void 0 ? {} : { scope },
|
|
787
788
|
parameters,
|
|
789
|
+
...uplink === void 0 ? {} : { uplink },
|
|
788
790
|
...cancellation === void 0 ? {} : { cancellation },
|
|
789
|
-
result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false, "undefined-or-void"),
|
|
791
|
+
result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false, "undefined-or-void", false, mode === void 0),
|
|
790
792
|
location: this.location(method.name)
|
|
791
793
|
};
|
|
792
794
|
}
|
|
@@ -865,10 +867,11 @@ var FaceAnalyzer = class {
|
|
|
865
867
|
if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
|
|
866
868
|
const [property] = argument.properties;
|
|
867
869
|
if (property === void 0) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
|
|
868
|
-
|
|
870
|
+
const mode = ts.isPropertyAssignment(property) && memberName(property.name) === "mode" ? stringLiteralValue(property.initializer) : void 0;
|
|
871
|
+
if (mode !== "stream") this.fail(property, "Remote() options must contain exactly mode: \"stream\"");
|
|
869
872
|
marker = {
|
|
870
873
|
kind: "direct",
|
|
871
|
-
mode
|
|
874
|
+
mode
|
|
872
875
|
};
|
|
873
876
|
}
|
|
874
877
|
} else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "RemoteScope")) {
|
|
@@ -889,18 +892,33 @@ var FaceAnalyzer = class {
|
|
|
889
892
|
}
|
|
890
893
|
return found;
|
|
891
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* The item types a Remote method's authored return type declares. Unary
|
|
897
|
+
* methods unwrap `Promise<T>`; stream methods unwrap `Iterable<Out>`,
|
|
898
|
+
* `AsyncIterable<Out>`, or the protocol's `RemoteStream<Out, In>`, whose
|
|
899
|
+
* second type argument is the uplink item type unless it is `never`.
|
|
900
|
+
*/
|
|
892
901
|
remoteResultType(method, mode) {
|
|
893
902
|
const authored = this.requiredType(method, method.type, "return");
|
|
894
903
|
if (ts.isTypeReferenceNode(authored)) {
|
|
895
904
|
const symbol = this.checker.getSymbolAtLocation(authored.typeName);
|
|
896
905
|
const resolved = symbol === void 0 ? void 0 : this.resolveSymbol(symbol);
|
|
897
|
-
const resultType = authored.typeArguments?.[0];
|
|
898
|
-
const wrappers = mode === "stream" ? ["Iterable", "AsyncIterable"] : ["Promise"];
|
|
899
906
|
const declaration = resolved === void 0 ? void 0 : preferredDeclaration(resolved);
|
|
900
|
-
|
|
907
|
+
const [result, uplink] = authored.typeArguments ?? [];
|
|
908
|
+
const arity = authored.typeArguments?.length ?? 0;
|
|
909
|
+
if (resolved !== void 0 && declaration !== void 0 && result !== void 0) {
|
|
910
|
+
if (isStandardLibraryFile(declaration.getSourceFile().fileName) && (mode === void 0 ? ["Promise"] : ["Iterable", "AsyncIterable"]).includes(resolved.name) && arity === 1) return { result };
|
|
911
|
+
if (mode !== void 0 && resolved.name === "RemoteStream" && this.isTypeMetaSymbol(authored.typeName, "RemoteStream") && arity <= 2) return uplink === void 0 || this.isNeverType(uplink) ? { result } : {
|
|
912
|
+
result,
|
|
913
|
+
uplink
|
|
914
|
+
};
|
|
915
|
+
}
|
|
901
916
|
}
|
|
902
|
-
if (mode
|
|
903
|
-
return authored;
|
|
917
|
+
if (mode !== void 0) this.fail(method, "stream Remote methods must return Iterable<Out>, AsyncIterable<Out>, or RemoteStream<Out, In>");
|
|
918
|
+
return { result: authored };
|
|
919
|
+
}
|
|
920
|
+
isNeverType(type) {
|
|
921
|
+
return (this.checker.getTypeFromTypeNode(type).flags & ts.TypeFlags.Never) !== 0;
|
|
904
922
|
}
|
|
905
923
|
isGlobalAbortSignal(type) {
|
|
906
924
|
const symbol = this.symbolAtType(type);
|
|
@@ -963,11 +981,11 @@ var FaceAnalyzer = class {
|
|
|
963
981
|
}
|
|
964
982
|
return result;
|
|
965
983
|
}
|
|
966
|
-
remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed, topLevelAbsence = "reject", optional = false) {
|
|
984
|
+
remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed, topLevelAbsence = "reject", optional = false, allowBytes = false) {
|
|
967
985
|
const type = this.convertType(authoredType);
|
|
968
986
|
const declaredType = this.checker.getTypeFromTypeNode(authoredType);
|
|
969
987
|
const resolvedType = optional ? this.checker.getNullableType(declaredType, ts.TypeFlags.Undefined) : declaredType;
|
|
970
|
-
const codecType = this.resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence);
|
|
988
|
+
const codecType = this.resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence, allowBytes);
|
|
971
989
|
const acceptsUndefined = topLevelAbsence !== "reject" && this.includesRemoteAbsence(resolvedType);
|
|
972
990
|
const rootSymbol = this.namedWorkspaceType(authoredType);
|
|
973
991
|
const imports = /* @__PURE__ */ new Map();
|
|
@@ -1012,8 +1030,8 @@ var FaceAnalyzer = class {
|
|
|
1012
1030
|
* validated without teaching the compiler-independent emitter TypeScript's
|
|
1013
1031
|
* type evaluator.
|
|
1014
1032
|
*/
|
|
1015
|
-
resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence) {
|
|
1016
|
-
this.assertRemoteJsonType(resolvedType, authoredType, /* @__PURE__ */ new Set(), topLevelAbsence !== "reject", topLevelAbsence === "undefined-or-void");
|
|
1033
|
+
resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence, allowBytes) {
|
|
1034
|
+
this.assertRemoteJsonType(resolvedType, authoredType, /* @__PURE__ */ new Set(), topLevelAbsence !== "reject", topLevelAbsence === "undefined-or-void", allowBytes);
|
|
1017
1035
|
const completed = /* @__PURE__ */ new Map();
|
|
1018
1036
|
const active = /* @__PURE__ */ new Map();
|
|
1019
1037
|
const recursiveDeclarations = /* @__PURE__ */ new Map();
|
|
@@ -1044,6 +1062,15 @@ var FaceAnalyzer = class {
|
|
|
1044
1062
|
return id;
|
|
1045
1063
|
};
|
|
1046
1064
|
const flags = type.flags;
|
|
1065
|
+
if (this.isRemoteByteArray(type)) return add({
|
|
1066
|
+
kind: "reference",
|
|
1067
|
+
name: "Uint8Array",
|
|
1068
|
+
target: {
|
|
1069
|
+
kind: "standard",
|
|
1070
|
+
name: "Uint8Array"
|
|
1071
|
+
},
|
|
1072
|
+
arguments: []
|
|
1073
|
+
});
|
|
1047
1074
|
if ((flags & ts.TypeFlags.Any) !== 0) return add({
|
|
1048
1075
|
kind: "keyword",
|
|
1049
1076
|
name: "any"
|
|
@@ -1127,7 +1154,7 @@ var FaceAnalyzer = class {
|
|
|
1127
1154
|
types: type.types.map(convert)
|
|
1128
1155
|
});
|
|
1129
1156
|
if ((flags & ts.TypeFlags.TypeParameter) !== 0) this.fail(authoredType, "Remote codec contains an unresolved type parameter");
|
|
1130
|
-
if ((flags & ts.TypeFlags.Object) === 0) this.fail(authoredType, `Remote codec type ${this.checker.typeToString(type, authoredType, ts.TypeFormatFlags.NoTruncation)} has no concrete Zod projection`);
|
|
1157
|
+
if ((flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) === 0) this.fail(authoredType, `Remote codec type ${this.checker.typeToString(type, authoredType, ts.TypeFormatFlags.NoTruncation)} has no concrete Zod projection`);
|
|
1131
1158
|
if (this.checker.isTupleType(type)) {
|
|
1132
1159
|
const reference = type;
|
|
1133
1160
|
const target = reference.target;
|
|
@@ -1136,7 +1163,10 @@ var FaceAnalyzer = class {
|
|
|
1136
1163
|
elements: this.checker.getTypeArguments(reference).map((argument, index) => {
|
|
1137
1164
|
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1138
1165
|
return {
|
|
1139
|
-
type:
|
|
1166
|
+
type: (elementFlags & ts.ElementFlags.Rest) !== 0 ? this.addNode(authoredType, {
|
|
1167
|
+
kind: "array",
|
|
1168
|
+
element: convert(argument)
|
|
1169
|
+
}) : convert(argument),
|
|
1140
1170
|
optional: (elementFlags & ts.ElementFlags.Optional) !== 0,
|
|
1141
1171
|
rest: (elementFlags & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0
|
|
1142
1172
|
};
|
|
@@ -1210,25 +1240,29 @@ var FaceAnalyzer = class {
|
|
|
1210
1240
|
};
|
|
1211
1241
|
return convert(resolvedType);
|
|
1212
1242
|
}
|
|
1213
|
-
assertRemoteJsonType(type, site, active, allowUndefined, allowVoid) {
|
|
1243
|
+
assertRemoteJsonType(type, site, active, allowUndefined, allowVoid, allowBytes = false) {
|
|
1214
1244
|
const flags = type.flags;
|
|
1215
1245
|
if ((flags & ts.TypeFlags.Undefined) !== 0 && allowUndefined) return;
|
|
1216
1246
|
if ((flags & ts.TypeFlags.Void) !== 0 && allowVoid) return;
|
|
1217
1247
|
if ((flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) this.fail(site, `Remote boundary contains unconstrained ${this.checker.typeToString(type)} data`);
|
|
1218
1248
|
if ((flags & (ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0) this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1219
1249
|
if ((flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.Null | ts.TypeFlags.Never)) !== 0) return;
|
|
1250
|
+
if (this.isRemoteByteArray(type)) {
|
|
1251
|
+
if (allowBytes) return;
|
|
1252
|
+
this.fail(site, "Remote Uint8Array is only supported in unary results");
|
|
1253
|
+
}
|
|
1220
1254
|
if (type.isUnion()) {
|
|
1221
|
-
for (const member of type.types) this.assertRemoteJsonType(member, site, active, allowUndefined, allowVoid);
|
|
1255
|
+
for (const member of type.types) this.assertRemoteJsonType(member, site, active, allowUndefined, allowVoid, allowBytes);
|
|
1222
1256
|
return;
|
|
1223
1257
|
}
|
|
1224
1258
|
if (type.isIntersection()) {
|
|
1225
1259
|
const material = type.types.filter((member) => !this.isRemotePhantomConstraint(member));
|
|
1226
1260
|
if (material.length === 0) this.fail(site, "Remote boundary contains a symbol-only object");
|
|
1227
|
-
for (const member of material) this.assertRemoteJsonType(member, site, active, false, false);
|
|
1261
|
+
for (const member of material) this.assertRemoteJsonType(member, site, active, false, false, allowBytes);
|
|
1228
1262
|
return;
|
|
1229
1263
|
}
|
|
1230
1264
|
if ((flags & ts.TypeFlags.TypeParameter) !== 0) this.fail(site, "Remote boundary contains an unresolved type parameter");
|
|
1231
|
-
if ((flags & ts.TypeFlags.Object) === 0) this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1265
|
+
if ((flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) === 0) this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1232
1266
|
const symbol = type.getSymbol();
|
|
1233
1267
|
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
1234
1268
|
if (declaration !== void 0 && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration))) this.fail(site, `Remote boundary contains class instance ${symbol?.name ?? this.checker.typeToString(type)}`);
|
|
@@ -1241,14 +1275,14 @@ var FaceAnalyzer = class {
|
|
|
1241
1275
|
const target = reference.target;
|
|
1242
1276
|
this.checker.getTypeArguments(reference).forEach((argument, index) => {
|
|
1243
1277
|
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1244
|
-
this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0, false);
|
|
1278
|
+
this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0, false, allowBytes);
|
|
1245
1279
|
});
|
|
1246
1280
|
return;
|
|
1247
1281
|
}
|
|
1248
1282
|
if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
|
|
1249
1283
|
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
1250
1284
|
if (element === void 0) this.fail(site, "Remote boundary array has no element type");
|
|
1251
|
-
this.assertRemoteJsonType(element, site, active, false, false);
|
|
1285
|
+
this.assertRemoteJsonType(element, site, active, false, false, allowBytes);
|
|
1252
1286
|
return;
|
|
1253
1287
|
}
|
|
1254
1288
|
const properties = this.checker.getPropertiesOfType(type);
|
|
@@ -1256,16 +1290,20 @@ var FaceAnalyzer = class {
|
|
|
1256
1290
|
for (const property of properties) {
|
|
1257
1291
|
const propertyDeclaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
1258
1292
|
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, propertyDeclaration ?? site);
|
|
1259
|
-
this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0, false);
|
|
1293
|
+
this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0, false, allowBytes);
|
|
1260
1294
|
}
|
|
1261
1295
|
for (const info of this.checker.getIndexInfosOfType(type)) {
|
|
1262
1296
|
if ((info.keyType.flags & ts.TypeFlags.ESSymbolLike) !== 0) this.fail(site, "Remote boundary contains a symbol index signature");
|
|
1263
|
-
this.assertRemoteJsonType(info.type, site, active, false, false);
|
|
1297
|
+
this.assertRemoteJsonType(info.type, site, active, false, false, allowBytes);
|
|
1264
1298
|
}
|
|
1265
1299
|
} finally {
|
|
1266
1300
|
active.delete(type);
|
|
1267
1301
|
}
|
|
1268
1302
|
}
|
|
1303
|
+
isRemoteByteArray(type) {
|
|
1304
|
+
const symbol = type.getSymbol();
|
|
1305
|
+
return symbol?.name === "Uint8Array" && symbol.declarations?.some((declaration) => isStandardLibraryFile(declaration.getSourceFile().fileName)) === true;
|
|
1306
|
+
}
|
|
1269
1307
|
includesRemoteAbsence(type) {
|
|
1270
1308
|
if ((type.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0) return true;
|
|
1271
1309
|
return type.isUnion() && type.types.some((member) => this.includesRemoteAbsence(member));
|
|
@@ -2904,8 +2942,13 @@ var FaceModelEmitter = class {
|
|
|
2904
2942
|
lines.push(" },");
|
|
2905
2943
|
});
|
|
2906
2944
|
lines.push(" ],");
|
|
2945
|
+
if (invocation.uplink !== void 0) {
|
|
2946
|
+
lines.push(" uplink: {");
|
|
2947
|
+
lines.push(` codec: ${indent(strictCodec(invocation.uplink.boundary, schemas.boundary(uplinkBoundaryKey(invocation))), 4).trimStart()},`);
|
|
2948
|
+
lines.push(" },");
|
|
2949
|
+
}
|
|
2907
2950
|
if (invocation.cancellation !== void 0) lines.push(" cancellation: { parameter: 'signal' },");
|
|
2908
|
-
lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation))), 2).trimStart()},`);
|
|
2951
|
+
lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation))), 2).trimStart()},`);
|
|
2909
2952
|
lines.push(` sourceLocation: ${JSON.stringify(invocation.location)},`);
|
|
2910
2953
|
lines.push("}");
|
|
2911
2954
|
return lines.join("\n");
|
|
@@ -2926,6 +2969,7 @@ var FaceModelEmitter = class {
|
|
|
2926
2969
|
"/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */",
|
|
2927
2970
|
"import type {",
|
|
2928
2971
|
" RemoteResult,",
|
|
2972
|
+
...packageModel.invocations.some((invocation) => invocation.mode === "stream") ? [" RemoteStreamHandle,"] : [],
|
|
2929
2973
|
" TypertRemoteContribution,",
|
|
2930
2974
|
"} from '@deepseek-ai/dsh-typert-protocol'"
|
|
2931
2975
|
];
|
|
@@ -2935,6 +2979,12 @@ var FaceModelEmitter = class {
|
|
|
2935
2979
|
lines.push(`import type { ${names.join(", ")} } from ${quote$1(specifier)}`);
|
|
2936
2980
|
}
|
|
2937
2981
|
lines.push("");
|
|
2982
|
+
if (packageModel.invocations.some((invocation) => containsBytes(this.renderer, invocation.result.codecType))) {
|
|
2983
|
+
lines.push("type RemoteDecoded<Value> = Value extends Uint8Array ? Uint8Array<ArrayBuffer>");
|
|
2984
|
+
lines.push(" : Value extends string | number | boolean | null | undefined ? Value");
|
|
2985
|
+
lines.push(" : { [Key in keyof Value]: RemoteDecoded<Value[Key]> }");
|
|
2986
|
+
lines.push("");
|
|
2987
|
+
}
|
|
2938
2988
|
lines.push("declare module '@deepseek-ai/dsh-typert-protocol' {");
|
|
2939
2989
|
const direct = packageModel.invocations.filter((invocation) => invocation.invocation.kind === "direct");
|
|
2940
2990
|
const scoped = packageModel.invocations.filter((invocation) => invocation.invocation.kind === "context" || invocation.scope !== void 0);
|
|
@@ -3005,8 +3055,12 @@ var FaceModelEmitter = class {
|
|
|
3005
3055
|
remoteFunctionType(invocation, referenceNames, scoped) {
|
|
3006
3056
|
const parameters = invocation.parameters.filter((parameter) => !scoped || invocation.invocation.kind === "context" || parameter.wire !== invocation.scope?.wire).map((parameter) => `${safeIdentifier(parameter.wire)}${parameter.optional === true ? "?" : ""}: ${this.renderer.renderType(parameter.boundary.type, referenceNames)}`);
|
|
3007
3057
|
if (invocation.cancellation !== void 0) parameters.push("signal?: AbortSignal");
|
|
3008
|
-
const
|
|
3009
|
-
|
|
3058
|
+
const authoredResult = this.renderer.renderType(invocation.result.type, referenceNames);
|
|
3059
|
+
const result = containsBytes(this.renderer, invocation.result.codecType) ? `RemoteDecoded<${authoredResult}>` : authoredResult;
|
|
3060
|
+
if (invocation.mode === "stream") {
|
|
3061
|
+
const uplink = invocation.uplink === void 0 ? "never" : this.renderer.renderType(invocation.uplink.boundary.type, referenceNames);
|
|
3062
|
+
return `(${parameters.join(", ")}) => RemoteStreamHandle<${result}, ${uplink}>`;
|
|
3063
|
+
}
|
|
3010
3064
|
return `(${parameters.join(", ")}) => Promise<RemoteResult<${result}>>`;
|
|
3011
3065
|
}
|
|
3012
3066
|
};
|
|
@@ -3021,6 +3075,100 @@ function uniqueNamespaces(invocations) {
|
|
|
3021
3075
|
function remoteNamespaceInterface(namespace) {
|
|
3022
3076
|
return `TypertRemoteNamespace$${Buffer.from(namespace, "utf8").toString("hex")}`;
|
|
3023
3077
|
}
|
|
3078
|
+
var ResultEncoderEmitter = class {
|
|
3079
|
+
renderer;
|
|
3080
|
+
names = /* @__PURE__ */ new Map();
|
|
3081
|
+
bodies = /* @__PURE__ */ new Map();
|
|
3082
|
+
constructor(renderer) {
|
|
3083
|
+
this.renderer = renderer;
|
|
3084
|
+
}
|
|
3085
|
+
reference(id) {
|
|
3086
|
+
if (!containsBytes(this.renderer, id)) return void 0;
|
|
3087
|
+
const existing = this.names.get(id);
|
|
3088
|
+
if (existing !== void 0) return existing;
|
|
3089
|
+
const name = `$encode${this.names.size}`;
|
|
3090
|
+
this.names.set(id, name);
|
|
3091
|
+
const node = this.renderer.node(id);
|
|
3092
|
+
const container = node.kind === "object" || node.kind === "array" || node.kind === "tuple";
|
|
3093
|
+
const body = container || node.kind === "union" || node.kind === "intersection" ? `return $resultContainer(value, path, ancestors, value => {\n${indent(this.body(id), 2)}\n}, ${container})` : this.body(id);
|
|
3094
|
+
this.bodies.set(id, `const ${name} = (value, writeBytes, path, ancestors) => {\n${indent(body, 2)}\n}`);
|
|
3095
|
+
return name;
|
|
3096
|
+
}
|
|
3097
|
+
definitions() {
|
|
3098
|
+
return this.bodies.size === 0 ? [] : [RESULT_SNAPSHOT, ...this.bodies.values()];
|
|
3099
|
+
}
|
|
3100
|
+
call(id, value, path = "path") {
|
|
3101
|
+
const reference = this.reference(id);
|
|
3102
|
+
return reference === void 0 ? value : `${reference}(${value}, writeBytes, ${path}, ancestors)`;
|
|
3103
|
+
}
|
|
3104
|
+
body(id) {
|
|
3105
|
+
const node = this.renderer.node(id);
|
|
3106
|
+
switch (node.kind) {
|
|
3107
|
+
case "reference":
|
|
3108
|
+
if (node.target.kind === "standard" && node.target.name === "Uint8Array") return "return value instanceof Uint8Array ? writeBytes(value, path) : value";
|
|
3109
|
+
if (node.target.kind === "declaration") {
|
|
3110
|
+
const type = this.renderer.declaration(node.target.symbol).type;
|
|
3111
|
+
if (type !== void 0) return `return ${this.call(type, "value")}`;
|
|
3112
|
+
}
|
|
3113
|
+
break;
|
|
3114
|
+
case "union":
|
|
3115
|
+
case "intersection": return [...node.types.filter((type) => containsBytes(this.renderer, type)).map((type) => `value = ${this.call(type, "value")}`), "return value"].join("\n");
|
|
3116
|
+
case "array": return [
|
|
3117
|
+
"if (!Array.isArray(value)) return value",
|
|
3118
|
+
`for (let index = 0; index < value.length; index++) value[index] = ${this.call(node.element, "value[index]", "[...path, index]")}`,
|
|
3119
|
+
"return value"
|
|
3120
|
+
].join("\n");
|
|
3121
|
+
case "tuple": {
|
|
3122
|
+
const lines = ["if (!Array.isArray(value)) return value"];
|
|
3123
|
+
for (const [index, element] of node.elements.entries()) {
|
|
3124
|
+
if (!containsBytes(this.renderer, element.type)) continue;
|
|
3125
|
+
if (element.rest) {
|
|
3126
|
+
const rest = this.renderer.node(element.type);
|
|
3127
|
+
if (rest.kind !== "array") break;
|
|
3128
|
+
lines.push(`for (let index = ${index}; index < value.length; index++) value[index] = ${this.call(rest.element, "value[index]", "[...path, index]")}`);
|
|
3129
|
+
} else lines.push(`if (value.length > ${index}) value[${index}] = ${this.call(element.type, `value[${index}]`, `[...path, ${index}]`)}`);
|
|
3130
|
+
}
|
|
3131
|
+
return [...lines, "return value"].join("\n");
|
|
3132
|
+
}
|
|
3133
|
+
case "object": {
|
|
3134
|
+
const lines = ["if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Uint8Array) return value"];
|
|
3135
|
+
for (const member of node.members) if (member.kind === "property" && containsBytes(this.renderer, member.type)) {
|
|
3136
|
+
const key = quote$1(member.name);
|
|
3137
|
+
lines.push(`if (Object.hasOwn(value, ${key})) value[${key}] = ${this.call(member.type, `value[${key}]`, `[...path, ${key}]`)}`);
|
|
3138
|
+
} else if (member.kind === "index" && containsBytes(this.renderer, member.signature.returns)) lines.push(`for (const key of Object.keys(value)) value[key] = ${this.call(member.signature.returns, "value[key]", "[...path, key]")}`);
|
|
3139
|
+
return [...lines, "return value"].join("\n");
|
|
3140
|
+
}
|
|
3141
|
+
default: break;
|
|
3142
|
+
}
|
|
3143
|
+
throw new TypertEmitError(`typert result encoder: ${node.kind} has no binary field projection`);
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
const RESULT_SNAPSHOT = `const $resultSnapshot = (input, path) => {
|
|
3147
|
+
if (input === null || typeof input !== 'object' || input instanceof Uint8Array) return input
|
|
3148
|
+
const toJSON = input.toJSON
|
|
3149
|
+
const value = typeof toJSON === 'function' ? toJSON.call(input, path.at(-1)?.toString() ?? 'value') : input
|
|
3150
|
+
if (value === null || typeof value !== 'object' || value instanceof Uint8Array) return value
|
|
3151
|
+
if (Array.isArray(value)) {
|
|
3152
|
+
const items = []
|
|
3153
|
+
for (let index = 0, length = value.length; index < length; index++) items.push(value[index])
|
|
3154
|
+
return items
|
|
3155
|
+
}
|
|
3156
|
+
const fields = {}
|
|
3157
|
+
for (const key of Object.keys(value)) {
|
|
3158
|
+
const item = key === 'toJSON' && value === input ? toJSON : value[key]
|
|
3159
|
+
if (key === 'toJSON' && typeof item === 'function') continue
|
|
3160
|
+
Object.defineProperty(fields, key, { value: item, enumerable: true, writable: true, configurable: true })
|
|
3161
|
+
}
|
|
3162
|
+
return fields
|
|
3163
|
+
}
|
|
3164
|
+
const $resultContainer = (input, path, ancestors, project, snapshot) => {
|
|
3165
|
+
if (input === null || typeof input !== 'object') return project(input)
|
|
3166
|
+
const owner = !ancestors.has(input)
|
|
3167
|
+
if (!owner && ancestors.get(input) !== path.length) throw new TypeError('Remote result contains a circular object')
|
|
3168
|
+
if (owner) ancestors.set(input, path.length)
|
|
3169
|
+
try { return project(snapshot ? $resultSnapshot(input, path) : input) }
|
|
3170
|
+
finally { if (owner) ancestors.delete(input) }
|
|
3171
|
+
}`;
|
|
3024
3172
|
var SchemaEmitter = class {
|
|
3025
3173
|
renderer;
|
|
3026
3174
|
schemas;
|
|
@@ -3056,10 +3204,15 @@ var SchemaEmitter = class {
|
|
|
3056
3204
|
}
|
|
3057
3205
|
emit() {
|
|
3058
3206
|
const definitions = this.declarations.map((declaration) => this.declarationDefinition(declaration));
|
|
3207
|
+
const encoder = new ResultEncoderEmitter(this.renderer);
|
|
3208
|
+
const encoders = /* @__PURE__ */ new Map();
|
|
3059
3209
|
for (const boundary of this.boundaries) {
|
|
3210
|
+
const encode = encoder.reference(boundary.type);
|
|
3211
|
+
if (encode !== void 0) encoders.set(boundary.key, encode);
|
|
3060
3212
|
const name = this.boundaryName(boundary.key);
|
|
3061
3213
|
definitions.push(`let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.typeSchema(boundary.type)})`);
|
|
3062
3214
|
}
|
|
3215
|
+
definitions.push(...encoder.definitions());
|
|
3063
3216
|
return {
|
|
3064
3217
|
definitions,
|
|
3065
3218
|
exports: this.schemas.map((model) => ({
|
|
@@ -3067,7 +3220,8 @@ var SchemaEmitter = class {
|
|
|
3067
3220
|
exportName: safeIdentifier(model.export.name),
|
|
3068
3221
|
internalName: this.exportSchemaName(model)
|
|
3069
3222
|
})),
|
|
3070
|
-
boundary: (key) => this.boundaryName(key)
|
|
3223
|
+
boundary: (key) => this.boundaryName(key),
|
|
3224
|
+
encoder: (key) => encoders.get(key)
|
|
3071
3225
|
};
|
|
3072
3226
|
}
|
|
3073
3227
|
declarationDefinition(declaration) {
|
|
@@ -3156,6 +3310,7 @@ var SchemaEmitter = class {
|
|
|
3156
3310
|
return `z.record(${this.typeSchema(key, substitutions)}, ${this.typeSchema(value, substitutions)})`;
|
|
3157
3311
|
}
|
|
3158
3312
|
case "Date": return "z.date()";
|
|
3313
|
+
case "Uint8Array": return "z.instanceof(Uint8Array)";
|
|
3159
3314
|
default: this.fail(node.name, `standard type ${node.target.name} has no Zod projection`);
|
|
3160
3315
|
}
|
|
3161
3316
|
this.fail(node.name, `${node.target.kind} reference has no Zod projection`);
|
|
@@ -3200,7 +3355,8 @@ var SchemaEmitter = class {
|
|
|
3200
3355
|
continue;
|
|
3201
3356
|
}
|
|
3202
3357
|
if (member.kind !== "property") this.fail(subject, `${member.kind} member ${member.name} is not data-schema projectable`);
|
|
3203
|
-
const
|
|
3358
|
+
const schema = this.readonly(this.typeSchema(member.type, substitutions), member.readonly && !containsBytes(this.renderer, member.type, false));
|
|
3359
|
+
const property = this.describe(this.optional(schema, member.optional), member);
|
|
3204
3360
|
properties.push(`${quote$1(member.jsonName ?? member.name)}: ${property}`);
|
|
3205
3361
|
}
|
|
3206
3362
|
if (indices.length > 1) this.fail(subject, "object type has more than one JSON index signature");
|
|
@@ -3279,6 +3435,10 @@ function invocationBoundaryRoots(invocations) {
|
|
|
3279
3435
|
type: parameter.boundary.codecType
|
|
3280
3436
|
});
|
|
3281
3437
|
});
|
|
3438
|
+
if (invocation.uplink !== void 0) result.push({
|
|
3439
|
+
key: uplinkBoundaryKey(invocation),
|
|
3440
|
+
type: invocation.uplink.boundary.codecType
|
|
3441
|
+
});
|
|
3282
3442
|
result.push({
|
|
3283
3443
|
key: resultBoundaryKey(invocation),
|
|
3284
3444
|
type: invocation.result.codecType
|
|
@@ -3289,18 +3449,38 @@ function invocationBoundaryRoots(invocations) {
|
|
|
3289
3449
|
function contextBoundaryKey(invocation) {
|
|
3290
3450
|
return `${invocation.id}:context`;
|
|
3291
3451
|
}
|
|
3452
|
+
function uplinkBoundaryKey(invocation) {
|
|
3453
|
+
return `${invocation.id}:uplink`;
|
|
3454
|
+
}
|
|
3292
3455
|
function parameterBoundaryKey(invocation, index) {
|
|
3293
3456
|
return `${invocation.id}:parameter:${String(index)}`;
|
|
3294
3457
|
}
|
|
3295
3458
|
function resultBoundaryKey(invocation) {
|
|
3296
3459
|
return `${invocation.id}:result`;
|
|
3297
3460
|
}
|
|
3298
|
-
function
|
|
3461
|
+
function containsBytes(renderer, id, nested = true, visited = /* @__PURE__ */ new Set()) {
|
|
3462
|
+
if (visited.has(id)) return false;
|
|
3463
|
+
visited.add(id);
|
|
3464
|
+
const node = renderer.node(id);
|
|
3465
|
+
if (node.kind === "reference") {
|
|
3466
|
+
if (node.target.kind === "standard" && node.target.name === "Uint8Array") return true;
|
|
3467
|
+
if (node.target.kind === "declaration") {
|
|
3468
|
+
const declaration = renderer.declaration(node.target.symbol);
|
|
3469
|
+
if (declaration.type !== void 0 && containsBytes(renderer, declaration.type, nested, visited)) return true;
|
|
3470
|
+
}
|
|
3471
|
+
if (!nested) return false;
|
|
3472
|
+
}
|
|
3473
|
+
if (!nested && node.kind !== "reference" && node.kind !== "parenthesized" && node.kind !== "union" && node.kind !== "intersection") return false;
|
|
3474
|
+
if (node.kind === "object" && node.members.some((member) => containsBytes(renderer, member.kind === "property" ? member.type : member.signature.returns, nested, visited))) return true;
|
|
3475
|
+
return childTypeNodeIds(node).some((child) => containsBytes(renderer, child, nested, visited));
|
|
3476
|
+
}
|
|
3477
|
+
function strictCodec(boundary, schema, encoder) {
|
|
3299
3478
|
return [
|
|
3300
3479
|
"{",
|
|
3301
3480
|
" mode: 'strict',",
|
|
3302
3481
|
` typeSymbol: ${quote$1(boundary.typeSymbol)},`,
|
|
3303
3482
|
` create: ${schema},`,
|
|
3483
|
+
...encoder === void 0 ? [] : [` decode: value => ${schema}().parse(value),`, ` encode: (value, writeBytes) => ${encoder}(value, writeBytes, [], new Map()),`],
|
|
3304
3484
|
"}"
|
|
3305
3485
|
].join("\n");
|
|
3306
3486
|
}
|
|
@@ -3316,12 +3496,19 @@ function remoteImports(invocations) {
|
|
|
3316
3496
|
for (const invocation of invocations) {
|
|
3317
3497
|
if (invocation.invocation.kind === "context") add(invocation.invocation.boundary);
|
|
3318
3498
|
for (const parameter of invocation.parameters) add(parameter.boundary);
|
|
3499
|
+
if (invocation.uplink !== void 0) add(invocation.uplink.boundary);
|
|
3319
3500
|
add(invocation.result);
|
|
3320
3501
|
}
|
|
3321
3502
|
return [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name));
|
|
3322
3503
|
}
|
|
3323
3504
|
function allocateRemoteImportNames(imports) {
|
|
3324
|
-
const used = new Set([
|
|
3505
|
+
const used = new Set([
|
|
3506
|
+
"RemoteDecoded",
|
|
3507
|
+
"RemoteResult",
|
|
3508
|
+
"RemoteStreamHandle",
|
|
3509
|
+
"TypertRemoteContribution",
|
|
3510
|
+
"TYPERT_REMOTE"
|
|
3511
|
+
]);
|
|
3325
3512
|
const names = /* @__PURE__ */ new Map();
|
|
3326
3513
|
for (const imported of imports) {
|
|
3327
3514
|
const base = safeIdentifier(imported.name);
|
package/lib/types/analyzer.js
CHANGED
|
@@ -965,7 +965,12 @@ class FaceAnalyzer {
|
|
|
965
965
|
}
|
|
966
966
|
}
|
|
967
967
|
const mode = invocation.kind === 'direct' ? invocation.mode : undefined;
|
|
968
|
-
const resultType = this.remoteResultType(method, mode);
|
|
968
|
+
const { result: resultType, uplink: uplinkType } = this.remoteResultType(method, mode);
|
|
969
|
+
const uplink = uplinkType === undefined
|
|
970
|
+
? undefined
|
|
971
|
+
: {
|
|
972
|
+
boundary: this.remoteBoundary(uplinkType, `${registration.name}#${binding.namespace}/${exportedMethod}:uplink`, false, 'undefined'),
|
|
973
|
+
};
|
|
969
974
|
return {
|
|
970
975
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
971
976
|
service: binding.service,
|
|
@@ -976,8 +981,9 @@ class FaceAnalyzer {
|
|
|
976
981
|
invocation: receiver,
|
|
977
982
|
...(scope === undefined ? {} : { scope }),
|
|
978
983
|
parameters,
|
|
984
|
+
...(uplink === undefined ? {} : { uplink }),
|
|
979
985
|
...(cancellation === undefined ? {} : { cancellation }),
|
|
980
|
-
result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false, 'undefined-or-void'),
|
|
986
|
+
result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false, 'undefined-or-void', false, mode === undefined),
|
|
981
987
|
location: this.location(method.name),
|
|
982
988
|
};
|
|
983
989
|
}
|
|
@@ -1098,12 +1104,12 @@ class FaceAnalyzer {
|
|
|
1098
1104
|
const [property] = argument.properties;
|
|
1099
1105
|
if (property === undefined)
|
|
1100
1106
|
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1107
|
+
const mode = ts.isPropertyAssignment(property) && memberName(property.name) === 'mode'
|
|
1108
|
+
? stringLiteralValue(property.initializer)
|
|
1109
|
+
: undefined;
|
|
1110
|
+
if (mode !== 'stream')
|
|
1104
1111
|
this.fail(property, 'Remote() options must contain exactly mode: "stream"');
|
|
1105
|
-
}
|
|
1106
|
-
marker = { kind: 'direct', mode: 'stream' };
|
|
1112
|
+
marker = { kind: 'direct', mode };
|
|
1107
1113
|
}
|
|
1108
1114
|
}
|
|
1109
1115
|
else if (ts.isCallExpression(expression)
|
|
@@ -1131,27 +1137,40 @@ class FaceAnalyzer {
|
|
|
1131
1137
|
}
|
|
1132
1138
|
return found;
|
|
1133
1139
|
}
|
|
1140
|
+
/**
|
|
1141
|
+
* The item types a Remote method's authored return type declares. Unary
|
|
1142
|
+
* methods unwrap `Promise<T>`; stream methods unwrap `Iterable<Out>`,
|
|
1143
|
+
* `AsyncIterable<Out>`, or the protocol's `RemoteStream<Out, In>`, whose
|
|
1144
|
+
* second type argument is the uplink item type unless it is `never`.
|
|
1145
|
+
*/
|
|
1134
1146
|
remoteResultType(method, mode) {
|
|
1135
1147
|
const authored = this.requiredType(method, method.type, 'return');
|
|
1136
1148
|
if (ts.isTypeReferenceNode(authored)) {
|
|
1137
1149
|
const symbol = this.checker.getSymbolAtLocation(authored.typeName);
|
|
1138
1150
|
const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
|
|
1139
|
-
const resultType = authored.typeArguments?.[0];
|
|
1140
|
-
const wrappers = mode === 'stream' ? ['Iterable', 'AsyncIterable'] : ['Promise'];
|
|
1141
1151
|
const declaration = resolved === undefined ? undefined : preferredDeclaration(resolved);
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
&&
|
|
1148
|
-
|
|
1152
|
+
const [result, uplink] = authored.typeArguments ?? [];
|
|
1153
|
+
const arity = authored.typeArguments?.length ?? 0;
|
|
1154
|
+
if (resolved !== undefined && declaration !== undefined && result !== undefined) {
|
|
1155
|
+
const standard = isStandardLibraryFile(declaration.getSourceFile().fileName);
|
|
1156
|
+
const wrappers = mode === undefined ? ['Promise'] : ['Iterable', 'AsyncIterable'];
|
|
1157
|
+
if (standard && wrappers.includes(resolved.name) && arity === 1)
|
|
1158
|
+
return { result };
|
|
1159
|
+
if (mode !== undefined
|
|
1160
|
+
&& resolved.name === 'RemoteStream'
|
|
1161
|
+
&& this.isTypeMetaSymbol(authored.typeName, 'RemoteStream')
|
|
1162
|
+
&& arity <= 2) {
|
|
1163
|
+
return uplink === undefined || this.isNeverType(uplink) ? { result } : { result, uplink };
|
|
1164
|
+
}
|
|
1149
1165
|
}
|
|
1150
1166
|
}
|
|
1151
|
-
if (mode
|
|
1152
|
-
this.fail(method, 'stream Remote methods must return Iterable<
|
|
1167
|
+
if (mode !== undefined) {
|
|
1168
|
+
this.fail(method, 'stream Remote methods must return Iterable<Out>, AsyncIterable<Out>, or RemoteStream<Out, In>');
|
|
1153
1169
|
}
|
|
1154
|
-
return authored;
|
|
1170
|
+
return { result: authored };
|
|
1171
|
+
}
|
|
1172
|
+
isNeverType(type) {
|
|
1173
|
+
return (this.checker.getTypeFromTypeNode(type).flags & ts.TypeFlags.Never) !== 0;
|
|
1155
1174
|
}
|
|
1156
1175
|
isGlobalAbortSignal(type) {
|
|
1157
1176
|
const symbol = this.symbolAtType(type);
|
|
@@ -1248,7 +1267,7 @@ class FaceAnalyzer {
|
|
|
1248
1267
|
}
|
|
1249
1268
|
return result;
|
|
1250
1269
|
}
|
|
1251
|
-
remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed, topLevelAbsence = 'reject', optional = false) {
|
|
1270
|
+
remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed, topLevelAbsence = 'reject', optional = false, allowBytes = false) {
|
|
1252
1271
|
const type = this.convertType(authoredType);
|
|
1253
1272
|
const declaredType = this.checker.getTypeFromTypeNode(authoredType);
|
|
1254
1273
|
// An optional parameter's authored node carries no `undefined`; the codec
|
|
@@ -1256,7 +1275,7 @@ class FaceAnalyzer {
|
|
|
1256
1275
|
const resolvedType = optional
|
|
1257
1276
|
? this.checker.getNullableType(declaredType, ts.TypeFlags.Undefined)
|
|
1258
1277
|
: declaredType;
|
|
1259
|
-
const codecType = this.resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence);
|
|
1278
|
+
const codecType = this.resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence, allowBytes);
|
|
1260
1279
|
const acceptsUndefined = topLevelAbsence !== 'reject' && this.includesRemoteAbsence(resolvedType);
|
|
1261
1280
|
const rootSymbol = this.namedWorkspaceType(authoredType);
|
|
1262
1281
|
const imports = new Map();
|
|
@@ -1306,8 +1325,8 @@ class FaceAnalyzer {
|
|
|
1306
1325
|
* validated without teaching the compiler-independent emitter TypeScript's
|
|
1307
1326
|
* type evaluator.
|
|
1308
1327
|
*/
|
|
1309
|
-
resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence) {
|
|
1310
|
-
this.assertRemoteJsonType(resolvedType, authoredType, new Set(), topLevelAbsence !== 'reject', topLevelAbsence === 'undefined-or-void');
|
|
1328
|
+
resolvedRemoteCodecType(authoredType, resolvedType, topLevelAbsence, allowBytes) {
|
|
1329
|
+
this.assertRemoteJsonType(resolvedType, authoredType, new Set(), topLevelAbsence !== 'reject', topLevelAbsence === 'undefined-or-void', allowBytes);
|
|
1311
1330
|
const completed = new Map();
|
|
1312
1331
|
const active = new Map();
|
|
1313
1332
|
const recursiveDeclarations = new Map();
|
|
@@ -1338,6 +1357,9 @@ class FaceAnalyzer {
|
|
|
1338
1357
|
return id;
|
|
1339
1358
|
};
|
|
1340
1359
|
const flags = type.flags;
|
|
1360
|
+
if (this.isRemoteByteArray(type)) {
|
|
1361
|
+
return add({ kind: 'reference', name: 'Uint8Array', target: { kind: 'standard', name: 'Uint8Array' }, arguments: [] });
|
|
1362
|
+
}
|
|
1341
1363
|
if ((flags & ts.TypeFlags.Any) !== 0)
|
|
1342
1364
|
return add({ kind: 'keyword', name: 'any' });
|
|
1343
1365
|
if ((flags & ts.TypeFlags.Unknown) !== 0)
|
|
@@ -1386,7 +1408,7 @@ class FaceAnalyzer {
|
|
|
1386
1408
|
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1387
1409
|
this.fail(authoredType, 'Remote codec contains an unresolved type parameter');
|
|
1388
1410
|
}
|
|
1389
|
-
if ((flags & ts.TypeFlags.Object) === 0) {
|
|
1411
|
+
if ((flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) === 0) {
|
|
1390
1412
|
this.fail(authoredType, `Remote codec type ${this.checker.typeToString(type, authoredType, ts.TypeFormatFlags.NoTruncation)} has no concrete Zod projection`);
|
|
1391
1413
|
}
|
|
1392
1414
|
if (this.checker.isTupleType(type)) {
|
|
@@ -1398,7 +1420,9 @@ class FaceAnalyzer {
|
|
|
1398
1420
|
elements: arguments_.map((argument, index) => {
|
|
1399
1421
|
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1400
1422
|
return {
|
|
1401
|
-
type:
|
|
1423
|
+
type: (elementFlags & ts.ElementFlags.Rest) !== 0
|
|
1424
|
+
? this.addNode(authoredType, { kind: 'array', element: convert(argument) })
|
|
1425
|
+
: convert(argument),
|
|
1402
1426
|
optional: (elementFlags & ts.ElementFlags.Optional) !== 0,
|
|
1403
1427
|
rest: (elementFlags & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0,
|
|
1404
1428
|
};
|
|
@@ -1472,7 +1496,7 @@ class FaceAnalyzer {
|
|
|
1472
1496
|
};
|
|
1473
1497
|
return convert(resolvedType);
|
|
1474
1498
|
}
|
|
1475
|
-
assertRemoteJsonType(type, site, active, allowUndefined, allowVoid) {
|
|
1499
|
+
assertRemoteJsonType(type, site, active, allowUndefined, allowVoid, allowBytes = false) {
|
|
1476
1500
|
const flags = type.flags;
|
|
1477
1501
|
if ((flags & ts.TypeFlags.Undefined) !== 0 && allowUndefined)
|
|
1478
1502
|
return;
|
|
@@ -1490,9 +1514,14 @@ class FaceAnalyzer {
|
|
|
1490
1514
|
| ts.TypeFlags.Null
|
|
1491
1515
|
| ts.TypeFlags.Never)) !== 0)
|
|
1492
1516
|
return;
|
|
1517
|
+
if (this.isRemoteByteArray(type)) {
|
|
1518
|
+
if (allowBytes)
|
|
1519
|
+
return;
|
|
1520
|
+
this.fail(site, 'Remote Uint8Array is only supported in unary results');
|
|
1521
|
+
}
|
|
1493
1522
|
if (type.isUnion()) {
|
|
1494
1523
|
for (const member of type.types) {
|
|
1495
|
-
this.assertRemoteJsonType(member, site, active, allowUndefined, allowVoid);
|
|
1524
|
+
this.assertRemoteJsonType(member, site, active, allowUndefined, allowVoid, allowBytes);
|
|
1496
1525
|
}
|
|
1497
1526
|
return;
|
|
1498
1527
|
}
|
|
@@ -1501,13 +1530,13 @@ class FaceAnalyzer {
|
|
|
1501
1530
|
if (material.length === 0)
|
|
1502
1531
|
this.fail(site, 'Remote boundary contains a symbol-only object');
|
|
1503
1532
|
for (const member of material)
|
|
1504
|
-
this.assertRemoteJsonType(member, site, active, false, false);
|
|
1533
|
+
this.assertRemoteJsonType(member, site, active, false, false, allowBytes);
|
|
1505
1534
|
return;
|
|
1506
1535
|
}
|
|
1507
1536
|
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1508
1537
|
this.fail(site, 'Remote boundary contains an unresolved type parameter');
|
|
1509
1538
|
}
|
|
1510
|
-
if ((flags & ts.TypeFlags.Object) === 0) {
|
|
1539
|
+
if ((flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) === 0) {
|
|
1511
1540
|
this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1512
1541
|
}
|
|
1513
1542
|
const symbol = type.getSymbol();
|
|
@@ -1528,7 +1557,7 @@ class FaceAnalyzer {
|
|
|
1528
1557
|
const arguments_ = this.checker.getTypeArguments(reference);
|
|
1529
1558
|
arguments_.forEach((argument, index) => {
|
|
1530
1559
|
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1531
|
-
this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0, false);
|
|
1560
|
+
this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0, false, allowBytes);
|
|
1532
1561
|
});
|
|
1533
1562
|
return;
|
|
1534
1563
|
}
|
|
@@ -1536,7 +1565,7 @@ class FaceAnalyzer {
|
|
|
1536
1565
|
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
1537
1566
|
if (element === undefined)
|
|
1538
1567
|
this.fail(site, 'Remote boundary array has no element type');
|
|
1539
|
-
this.assertRemoteJsonType(element, site, active, false, false);
|
|
1568
|
+
this.assertRemoteJsonType(element, site, active, false, false, allowBytes);
|
|
1540
1569
|
return;
|
|
1541
1570
|
}
|
|
1542
1571
|
const properties = this.checker.getPropertiesOfType(type);
|
|
@@ -1546,19 +1575,24 @@ class FaceAnalyzer {
|
|
|
1546
1575
|
for (const property of properties) {
|
|
1547
1576
|
const propertyDeclaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
1548
1577
|
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, propertyDeclaration ?? site);
|
|
1549
|
-
this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0, false);
|
|
1578
|
+
this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0, false, allowBytes);
|
|
1550
1579
|
}
|
|
1551
1580
|
for (const info of this.checker.getIndexInfosOfType(type)) {
|
|
1552
1581
|
if ((info.keyType.flags & ts.TypeFlags.ESSymbolLike) !== 0) {
|
|
1553
1582
|
this.fail(site, 'Remote boundary contains a symbol index signature');
|
|
1554
1583
|
}
|
|
1555
|
-
this.assertRemoteJsonType(info.type, site, active, false, false);
|
|
1584
|
+
this.assertRemoteJsonType(info.type, site, active, false, false, allowBytes);
|
|
1556
1585
|
}
|
|
1557
1586
|
}
|
|
1558
1587
|
finally {
|
|
1559
1588
|
active.delete(type);
|
|
1560
1589
|
}
|
|
1561
1590
|
}
|
|
1591
|
+
isRemoteByteArray(type) {
|
|
1592
|
+
const symbol = type.getSymbol();
|
|
1593
|
+
return symbol?.name === 'Uint8Array'
|
|
1594
|
+
&& symbol.declarations?.some(declaration => isStandardLibraryFile(declaration.getSourceFile().fileName)) === true;
|
|
1595
|
+
}
|
|
1562
1596
|
includesRemoteAbsence(type) {
|
|
1563
1597
|
if ((type.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0)
|
|
1564
1598
|
return true;
|
package/lib/types/emitter.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Buffer } from 'node:buffer';
|
|
|
7
7
|
import { posix } from 'node:path';
|
|
8
8
|
import { GenMapping, addMapping, toEncodedMap } from '@jridgewell/gen-mapping';
|
|
9
9
|
import { TypeGraphRenderer } from "./renderer.js";
|
|
10
|
+
import { childTypeNodeIds } from "./model.js";
|
|
10
11
|
/** Failure to project a modeled construct into an emitted artifact. */
|
|
11
12
|
export class TypertEmitError extends Error {
|
|
12
13
|
name = 'TypertEmitError';
|
|
@@ -231,10 +232,15 @@ export class FaceModelEmitter {
|
|
|
231
232
|
lines.push(' },');
|
|
232
233
|
});
|
|
233
234
|
lines.push(' ],');
|
|
235
|
+
if (invocation.uplink !== undefined) {
|
|
236
|
+
lines.push(' uplink: {');
|
|
237
|
+
lines.push(` codec: ${indent(strictCodec(invocation.uplink.boundary, schemas.boundary(uplinkBoundaryKey(invocation))), 4).trimStart()},`);
|
|
238
|
+
lines.push(' },');
|
|
239
|
+
}
|
|
234
240
|
if (invocation.cancellation !== undefined) {
|
|
235
241
|
lines.push(" cancellation: { parameter: 'signal' },");
|
|
236
242
|
}
|
|
237
|
-
lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation))), 2).trimStart()},`);
|
|
243
|
+
lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation))), 2).trimStart()},`);
|
|
238
244
|
lines.push(` sourceLocation: ${JSON.stringify(invocation.location)},`);
|
|
239
245
|
lines.push('}');
|
|
240
246
|
return lines.join('\n');
|
|
@@ -251,10 +257,13 @@ export class FaceModelEmitter {
|
|
|
251
257
|
});
|
|
252
258
|
grouped.set(imported.specifier, values);
|
|
253
259
|
}
|
|
260
|
+
// A stream method's Client signature returns the handle; `RemoteStream` names
|
|
261
|
+
// what the Host method returns.
|
|
254
262
|
const lines = [
|
|
255
263
|
'/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */',
|
|
256
264
|
'import type {',
|
|
257
265
|
' RemoteResult,',
|
|
266
|
+
...packageModel.invocations.some(invocation => invocation.mode === 'stream') ? [' RemoteStreamHandle,'] : [],
|
|
258
267
|
' TypertRemoteContribution,',
|
|
259
268
|
'} from \'@deepseek-ai/dsh-typert-protocol\'',
|
|
260
269
|
];
|
|
@@ -264,6 +273,12 @@ export class FaceModelEmitter {
|
|
|
264
273
|
lines.push(`import type { ${names.join(', ')} } from ${quote(specifier)}`);
|
|
265
274
|
}
|
|
266
275
|
lines.push('');
|
|
276
|
+
if (packageModel.invocations.some(invocation => containsBytes(this.renderer, invocation.result.codecType))) {
|
|
277
|
+
lines.push('type RemoteDecoded<Value> = Value extends Uint8Array ? Uint8Array<ArrayBuffer>');
|
|
278
|
+
lines.push(' : Value extends string | number | boolean | null | undefined ? Value');
|
|
279
|
+
lines.push(' : { [Key in keyof Value]: RemoteDecoded<Value[Key]> }');
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
267
282
|
lines.push('declare module \'@deepseek-ai/dsh-typert-protocol\' {');
|
|
268
283
|
const direct = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'direct');
|
|
269
284
|
const scoped = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'context' || invocation.scope !== undefined);
|
|
@@ -342,9 +357,15 @@ export class FaceModelEmitter {
|
|
|
342
357
|
const parameters = invocation.parameters.filter(parameter => !scoped || invocation.invocation.kind === 'context' || parameter.wire !== invocation.scope?.wire).map(parameter => `${safeIdentifier(parameter.wire)}${parameter.optional === true ? '?' : ''}: ${this.renderer.renderType(parameter.boundary.type, referenceNames)}`);
|
|
343
358
|
if (invocation.cancellation !== undefined)
|
|
344
359
|
parameters.push('signal?: AbortSignal');
|
|
345
|
-
const
|
|
360
|
+
const authoredResult = this.renderer.renderType(invocation.result.type, referenceNames);
|
|
361
|
+
const result = containsBytes(this.renderer, invocation.result.codecType)
|
|
362
|
+
? `RemoteDecoded<${authoredResult}>`
|
|
363
|
+
: authoredResult;
|
|
346
364
|
if (invocation.mode === 'stream') {
|
|
347
|
-
|
|
365
|
+
const uplink = invocation.uplink === undefined
|
|
366
|
+
? 'never'
|
|
367
|
+
: this.renderer.renderType(invocation.uplink.boundary.type, referenceNames);
|
|
368
|
+
return `(${parameters.join(', ')}) => RemoteStreamHandle<${result}, ${uplink}>`;
|
|
348
369
|
}
|
|
349
370
|
// The unary Client Remote face delivers the carrier's outcome, so every
|
|
350
371
|
// generated consumer signature resolves to a result the caller reads.
|
|
@@ -364,6 +385,121 @@ function uniqueNamespaces(invocations) {
|
|
|
364
385
|
function remoteNamespaceInterface(namespace) {
|
|
365
386
|
return `TypertRemoteNamespace$${Buffer.from(namespace, 'utf8').toString('hex')}`;
|
|
366
387
|
}
|
|
388
|
+
class ResultEncoderEmitter {
|
|
389
|
+
renderer;
|
|
390
|
+
names = new Map();
|
|
391
|
+
bodies = new Map();
|
|
392
|
+
constructor(renderer) {
|
|
393
|
+
this.renderer = renderer;
|
|
394
|
+
}
|
|
395
|
+
reference(id) {
|
|
396
|
+
if (!containsBytes(this.renderer, id))
|
|
397
|
+
return undefined;
|
|
398
|
+
const existing = this.names.get(id);
|
|
399
|
+
if (existing !== undefined)
|
|
400
|
+
return existing;
|
|
401
|
+
const name = `$encode${this.names.size}`;
|
|
402
|
+
this.names.set(id, name);
|
|
403
|
+
const node = this.renderer.node(id);
|
|
404
|
+
const container = node.kind === 'object' || node.kind === 'array' || node.kind === 'tuple';
|
|
405
|
+
const body = container || node.kind === 'union' || node.kind === 'intersection'
|
|
406
|
+
? `return $resultContainer(value, path, ancestors, value => {\n${indent(this.body(id), 2)}\n}, ${container})`
|
|
407
|
+
: this.body(id);
|
|
408
|
+
this.bodies.set(id, `const ${name} = (value, writeBytes, path, ancestors) => {\n${indent(body, 2)}\n}`);
|
|
409
|
+
return name;
|
|
410
|
+
}
|
|
411
|
+
definitions() {
|
|
412
|
+
return this.bodies.size === 0 ? [] : [RESULT_SNAPSHOT, ...this.bodies.values()];
|
|
413
|
+
}
|
|
414
|
+
call(id, value, path = 'path') {
|
|
415
|
+
const reference = this.reference(id);
|
|
416
|
+
return reference === undefined ? value : `${reference}(${value}, writeBytes, ${path}, ancestors)`;
|
|
417
|
+
}
|
|
418
|
+
body(id) {
|
|
419
|
+
const node = this.renderer.node(id);
|
|
420
|
+
switch (node.kind) {
|
|
421
|
+
case 'reference': {
|
|
422
|
+
if (node.target.kind === 'standard' && node.target.name === 'Uint8Array') {
|
|
423
|
+
return 'return value instanceof Uint8Array ? writeBytes(value, path) : value';
|
|
424
|
+
}
|
|
425
|
+
if (node.target.kind === 'declaration') {
|
|
426
|
+
const type = this.renderer.declaration(node.target.symbol).type;
|
|
427
|
+
if (type !== undefined)
|
|
428
|
+
return `return ${this.call(type, 'value')}`;
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case 'union':
|
|
433
|
+
case 'intersection': return [
|
|
434
|
+
...node.types.filter(type => containsBytes(this.renderer, type)).map(type => `value = ${this.call(type, 'value')}`),
|
|
435
|
+
'return value',
|
|
436
|
+
].join('\n');
|
|
437
|
+
case 'array': return [
|
|
438
|
+
'if (!Array.isArray(value)) return value',
|
|
439
|
+
`for (let index = 0; index < value.length; index++) value[index] = ${this.call(node.element, 'value[index]', '[...path, index]')}`,
|
|
440
|
+
'return value',
|
|
441
|
+
].join('\n');
|
|
442
|
+
case 'tuple': {
|
|
443
|
+
const lines = ['if (!Array.isArray(value)) return value'];
|
|
444
|
+
for (const [index, element] of node.elements.entries()) {
|
|
445
|
+
if (!containsBytes(this.renderer, element.type))
|
|
446
|
+
continue;
|
|
447
|
+
if (element.rest) {
|
|
448
|
+
const rest = this.renderer.node(element.type);
|
|
449
|
+
if (rest.kind !== 'array')
|
|
450
|
+
break;
|
|
451
|
+
lines.push(`for (let index = ${index}; index < value.length; index++) value[index] = ${this.call(rest.element, 'value[index]', '[...path, index]')}`);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
lines.push(`if (value.length > ${index}) value[${index}] = ${this.call(element.type, `value[${index}]`, `[...path, ${index}]`)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return [...lines, 'return value'].join('\n');
|
|
458
|
+
}
|
|
459
|
+
case 'object': {
|
|
460
|
+
const lines = ["if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Uint8Array) return value"];
|
|
461
|
+
for (const member of node.members) {
|
|
462
|
+
if (member.kind === 'property' && containsBytes(this.renderer, member.type)) {
|
|
463
|
+
const key = quote(member.name);
|
|
464
|
+
lines.push(`if (Object.hasOwn(value, ${key})) value[${key}] = ${this.call(member.type, `value[${key}]`, `[...path, ${key}]`)}`);
|
|
465
|
+
}
|
|
466
|
+
else if (member.kind === 'index' && containsBytes(this.renderer, member.signature.returns)) {
|
|
467
|
+
lines.push(`for (const key of Object.keys(value)) value[key] = ${this.call(member.signature.returns, 'value[key]', '[...path, key]')}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return [...lines, 'return value'].join('\n');
|
|
471
|
+
}
|
|
472
|
+
default: break;
|
|
473
|
+
}
|
|
474
|
+
throw new TypertEmitError(`typert result encoder: ${node.kind} has no binary field projection`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const RESULT_SNAPSHOT = `const $resultSnapshot = (input, path) => {
|
|
478
|
+
if (input === null || typeof input !== 'object' || input instanceof Uint8Array) return input
|
|
479
|
+
const toJSON = input.toJSON
|
|
480
|
+
const value = typeof toJSON === 'function' ? toJSON.call(input, path.at(-1)?.toString() ?? 'value') : input
|
|
481
|
+
if (value === null || typeof value !== 'object' || value instanceof Uint8Array) return value
|
|
482
|
+
if (Array.isArray(value)) {
|
|
483
|
+
const items = []
|
|
484
|
+
for (let index = 0, length = value.length; index < length; index++) items.push(value[index])
|
|
485
|
+
return items
|
|
486
|
+
}
|
|
487
|
+
const fields = {}
|
|
488
|
+
for (const key of Object.keys(value)) {
|
|
489
|
+
const item = key === 'toJSON' && value === input ? toJSON : value[key]
|
|
490
|
+
if (key === 'toJSON' && typeof item === 'function') continue
|
|
491
|
+
Object.defineProperty(fields, key, { value: item, enumerable: true, writable: true, configurable: true })
|
|
492
|
+
}
|
|
493
|
+
return fields
|
|
494
|
+
}
|
|
495
|
+
const $resultContainer = (input, path, ancestors, project, snapshot) => {
|
|
496
|
+
if (input === null || typeof input !== 'object') return project(input)
|
|
497
|
+
const owner = !ancestors.has(input)
|
|
498
|
+
if (!owner && ancestors.get(input) !== path.length) throw new TypeError('Remote result contains a circular object')
|
|
499
|
+
if (owner) ancestors.set(input, path.length)
|
|
500
|
+
try { return project(snapshot ? $resultSnapshot(input, path) : input) }
|
|
501
|
+
finally { if (owner) ancestors.delete(input) }
|
|
502
|
+
}`;
|
|
367
503
|
class SchemaEmitter {
|
|
368
504
|
renderer;
|
|
369
505
|
schemas;
|
|
@@ -409,10 +545,16 @@ class SchemaEmitter {
|
|
|
409
545
|
}
|
|
410
546
|
emit() {
|
|
411
547
|
const definitions = this.declarations.map(declaration => this.declarationDefinition(declaration));
|
|
548
|
+
const encoder = new ResultEncoderEmitter(this.renderer);
|
|
549
|
+
const encoders = new Map();
|
|
412
550
|
for (const boundary of this.boundaries) {
|
|
551
|
+
const encode = encoder.reference(boundary.type);
|
|
552
|
+
if (encode !== undefined)
|
|
553
|
+
encoders.set(boundary.key, encode);
|
|
413
554
|
const name = this.boundaryName(boundary.key);
|
|
414
555
|
definitions.push(`let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.typeSchema(boundary.type)})`);
|
|
415
556
|
}
|
|
557
|
+
definitions.push(...encoder.definitions());
|
|
416
558
|
const exports = this.schemas.map((model) => ({
|
|
417
559
|
model,
|
|
418
560
|
exportName: safeIdentifier(model.export.name),
|
|
@@ -422,6 +564,7 @@ class SchemaEmitter {
|
|
|
422
564
|
definitions,
|
|
423
565
|
exports,
|
|
424
566
|
boundary: key => this.boundaryName(key),
|
|
567
|
+
encoder: key => encoders.get(key),
|
|
425
568
|
};
|
|
426
569
|
}
|
|
427
570
|
declarationDefinition(declaration) {
|
|
@@ -531,6 +674,7 @@ class SchemaEmitter {
|
|
|
531
674
|
return `z.record(${this.typeSchema(key, substitutions)}, ${this.typeSchema(value, substitutions)})`;
|
|
532
675
|
}
|
|
533
676
|
case 'Date': return 'z.date()';
|
|
677
|
+
case 'Uint8Array': return 'z.instanceof(Uint8Array)';
|
|
534
678
|
default: this.fail(node.name, `standard type ${node.target.name} has no Zod projection`);
|
|
535
679
|
}
|
|
536
680
|
}
|
|
@@ -592,7 +736,9 @@ class SchemaEmitter {
|
|
|
592
736
|
}
|
|
593
737
|
if (member.kind !== 'property')
|
|
594
738
|
this.fail(subject, `${member.kind} member ${member.name} is not data-schema projectable`);
|
|
595
|
-
|
|
739
|
+
// Freezing a union may freeze its populated Uint8Array member.
|
|
740
|
+
const schema = this.readonly(this.typeSchema(member.type, substitutions), member.readonly && !containsBytes(this.renderer, member.type, false));
|
|
741
|
+
const property = this.describe(this.optional(schema, member.optional), member);
|
|
596
742
|
properties.push(`${quote(member.jsonName ?? member.name)}: ${property}`);
|
|
597
743
|
}
|
|
598
744
|
if (indices.length > 1)
|
|
@@ -677,6 +823,9 @@ function invocationBoundaryRoots(invocations) {
|
|
|
677
823
|
invocation.parameters.forEach((parameter, index) => {
|
|
678
824
|
result.push({ key: parameterBoundaryKey(invocation, index), type: parameter.boundary.codecType });
|
|
679
825
|
});
|
|
826
|
+
if (invocation.uplink !== undefined) {
|
|
827
|
+
result.push({ key: uplinkBoundaryKey(invocation), type: invocation.uplink.boundary.codecType });
|
|
828
|
+
}
|
|
680
829
|
result.push({ key: resultBoundaryKey(invocation), type: invocation.result.codecType });
|
|
681
830
|
}
|
|
682
831
|
return result;
|
|
@@ -684,18 +833,48 @@ function invocationBoundaryRoots(invocations) {
|
|
|
684
833
|
function contextBoundaryKey(invocation) {
|
|
685
834
|
return `${invocation.id}:context`;
|
|
686
835
|
}
|
|
836
|
+
function uplinkBoundaryKey(invocation) {
|
|
837
|
+
return `${invocation.id}:uplink`;
|
|
838
|
+
}
|
|
687
839
|
function parameterBoundaryKey(invocation, index) {
|
|
688
840
|
return `${invocation.id}:parameter:${String(index)}`;
|
|
689
841
|
}
|
|
690
842
|
function resultBoundaryKey(invocation) {
|
|
691
843
|
return `${invocation.id}:result`;
|
|
692
844
|
}
|
|
693
|
-
function
|
|
845
|
+
function containsBytes(renderer, id, nested = true, visited = new Set()) {
|
|
846
|
+
if (visited.has(id))
|
|
847
|
+
return false;
|
|
848
|
+
visited.add(id);
|
|
849
|
+
const node = renderer.node(id);
|
|
850
|
+
if (node.kind === 'reference') {
|
|
851
|
+
if (node.target.kind === 'standard' && node.target.name === 'Uint8Array')
|
|
852
|
+
return true;
|
|
853
|
+
if (node.target.kind === 'declaration') {
|
|
854
|
+
const declaration = renderer.declaration(node.target.symbol);
|
|
855
|
+
if (declaration.type !== undefined && containsBytes(renderer, declaration.type, nested, visited))
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
if (!nested)
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
if (!nested && node.kind !== 'reference' && node.kind !== 'parenthesized'
|
|
862
|
+
&& node.kind !== 'union' && node.kind !== 'intersection')
|
|
863
|
+
return false;
|
|
864
|
+
if (node.kind === 'object' && node.members.some(member => containsBytes(renderer, member.kind === 'property' ? member.type : member.signature.returns, nested, visited)))
|
|
865
|
+
return true;
|
|
866
|
+
return childTypeNodeIds(node).some(child => containsBytes(renderer, child, nested, visited));
|
|
867
|
+
}
|
|
868
|
+
function strictCodec(boundary, schema, encoder) {
|
|
694
869
|
return [
|
|
695
870
|
'{',
|
|
696
871
|
' mode: \'strict\',',
|
|
697
872
|
` typeSymbol: ${quote(boundary.typeSymbol)},`,
|
|
698
873
|
` create: ${schema},`,
|
|
874
|
+
...(encoder === undefined ? [] : [
|
|
875
|
+
` decode: value => ${schema}().parse(value),`,
|
|
876
|
+
` encode: (value, writeBytes) => ${encoder}(value, writeBytes, [], new Map()),`,
|
|
877
|
+
]),
|
|
699
878
|
'}',
|
|
700
879
|
].join('\n');
|
|
701
880
|
}
|
|
@@ -716,12 +895,20 @@ function remoteImports(invocations) {
|
|
|
716
895
|
add(invocation.invocation.boundary);
|
|
717
896
|
for (const parameter of invocation.parameters)
|
|
718
897
|
add(parameter.boundary);
|
|
898
|
+
if (invocation.uplink !== undefined)
|
|
899
|
+
add(invocation.uplink.boundary);
|
|
719
900
|
add(invocation.result);
|
|
720
901
|
}
|
|
721
902
|
return [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name));
|
|
722
903
|
}
|
|
723
904
|
function allocateRemoteImportNames(imports) {
|
|
724
|
-
const used = new Set([
|
|
905
|
+
const used = new Set([
|
|
906
|
+
'RemoteDecoded',
|
|
907
|
+
'RemoteResult',
|
|
908
|
+
'RemoteStreamHandle',
|
|
909
|
+
'TypertRemoteContribution',
|
|
910
|
+
'TYPERT_REMOTE',
|
|
911
|
+
]);
|
|
725
912
|
const names = new Map();
|
|
726
913
|
for (const imported of imports) {
|
|
727
914
|
const base = safeIdentifier(imported.name);
|
package/lib/types/model.d.ts
CHANGED
|
@@ -117,6 +117,10 @@ export interface InvocationModel {
|
|
|
117
117
|
readonly wire: string;
|
|
118
118
|
};
|
|
119
119
|
readonly parameters: readonly InvocationParameterModel[];
|
|
120
|
+
/** Per-item boundary of the Client uplink, from the `In` type argument of `RemoteStream<Out, In>`; absent when the method reads none. */
|
|
121
|
+
readonly uplink?: {
|
|
122
|
+
readonly boundary: RemoteBoundaryModel;
|
|
123
|
+
};
|
|
120
124
|
readonly cancellation?: {
|
|
121
125
|
readonly parameter: 'signal';
|
|
122
126
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-typert-generator",
|
|
3
3
|
"description": "TypeScript project analyzer and model-driven Typert artifact generator",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"typescript": "^6.0.3"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/cordis": "
|
|
39
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"zod": "^4.4.3",
|
|
43
|
-
"@deepseek-ai/dsh-tool-cordis": "
|
|
44
|
-
"@deepseek-ai/dsh-typert-registry": "
|
|
45
|
-
"@deepseek-ai/cordis": "
|
|
43
|
+
"@deepseek-ai/dsh-tool-cordis": "0.1.7-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-typert-registry": "0.1.7-alpha.2",
|
|
45
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
46
46
|
}
|
|
47
47
|
}
|