40 KiB
TypeScript API Model
Status
TypeScriptの型は、通常のJavaScript outputには残らない。WIP Hostがruntimeに必要とするInterface Descriptorやvalidationをどう定義するか。このdraftは、TypeScriptでHostを実装するための概念的なAPI modelを提案する。
Function、object shape、generic type、class、method signatureは説明用であり、具体的なTypeScript API syntaxは確定していない。Package構成、schema builderの最終名称、ownership、async context、error type、bundler integrationも未決定である。
Protocol上の確定事項はCore Model、Object tree / Schema、Operation and Interface、Protocolに従う。動的なObject projectionの運用要件はDynamic Worldspace Projection、対応するRust modelはRust API Modelを参照する。
目的
TypeScript APIでは、runtimeに残るdefinition objectを使い、Protocol上の宣言とTypeScript型の推論を対応付けるmodelを自然に表現できるようにする。Objectの実装と、ある時点で公開するInterfaceの選択は分けて扱う。
- Runtimeに残るdefinition objectでWIP Interfaceを宣言する。
- 同じdefinition objectからInterface Descriptor、Value codec、validationを構築する。
- Definition objectからOperation implementationのTypeScript型を推論する。
- Classまたはplain objectが、Protocol Objectとして公開するmetadataと、必要に応じて0個以上のInterfaceのOperation implementationを提供する。
- Objectを返す時点で、公開するInterfaceのordered setを明示的に選択する。
- Object instanceをWorldspace全体について常駐させず、
observeとcall_operationの解決時にdata-drivenに生成できる。 - Custom transpiler、TypeScript compiler plugin、decorator metadataをcore APIの前提にしない。
Protocol上でObjectとInterfaceを関連付けるのはObject.interfacesだけである。Operation implementationを呼び出すためのTypeScript / JavaScript内部codeはSDK / Host Runtimeの実装詳細として扱い、新しいProtocol conceptは導入しない。
TypeScriptの型消去
TypeScriptのinterface、type、generic type argumentは、通常のJavaScript outputには残らない。たとえば、次のdeclarationだけからruntimeにInterface Descriptorを取得することはできない。
interface FileOperations {
read(args: ReadArguments): Promise<Uint8Array>;
}
host.registerInterface(FileOperations); // runtime valueではない
そこで、TypeScript APIではruntime valueをsource of truthにする。
const FileInterface = wip.interface({
// runtime schema
});
このobjectは、通常のJavaScript valueとしてtranspile後も残る。TypeScript型は、このruntime definitionから推論する。
Conceptual model
Type definition
Record、Enum、Union等の型も、runtime definition objectとして宣言する。
const WriteResult = wip.union({
name: "WriteResult",
cases: {
written: wip.record({
bytesWritten: wip.integer(),
}),
rejected: wip.record({
reason: wip.string(),
}),
},
});
このdefinitionから、TypeScript型を推論する。
type WriteResult = wip.Infer<typeof WriteResult>;
wip.Inferの具体的な型実装とUnion DTOのTypeScript representationは、まだ決めていない。Runtime definitionと推論した型が、同じProtocol TypeExprおよびValue semanticsを表すことを要件とする。
Interface definition
WIP Interfaceも、runtime definition objectとして宣言する。
const FileInterface = wip.interface({
documentation: {
summary: "Operations available on a file",
},
types: {
WriteResult,
},
operations: {
read: wip.operation({
name: "read",
documentation: {
summary: "Reads bytes from the file",
},
parameters: {
offset: wip.optional(wip.integer()),
length: wip.optional(wip.integer()),
},
returns: wip.bytes(),
}),
write: wip.operation({
name: "write",
documentation: {
summary: "Replaces the file contents",
},
parameters: {
content: wip.bytes(),
},
returns: WriteResult,
}),
},
});
このsyntaxはconceptualであり、具体的なAPIとして確定したものではない。Definition objectのproperty名、documentationの指定方法、Named typeの参照方法、optionalの表現は未決定である。
Interface definitionとProtocol上の宣言は、次のように対応付ける。
- 一つのruntime definition objectを、一つのInterface Descriptorを構築するTypeScript側definitionとして扱う。
- JavaScript variable名
FileInterfaceはlocal code上の識別子であり、ProtocolInterfaceDescriptorのfieldや構造化されたInterface参照としてそのまま公開しない。 - 各Operation definitionが保持するstableなstringをOperation Declarationの
nameとして扱う。 parametersで宣言されたstableなstringを名前付きOperation parameterとして扱う。returnsをOperation return typeとして扱う。- Runtime type definitionをDescriptorの
TypeExprとProtocolValueへ対応付ける。 - DocumentationをInterface、Operation、parameter、return、type、field、caseへ指定できるようにする。
Descriptorのshapeを決めるのはruntime definition objectである。各classまたはplain objectがどう実装するかによって、Descriptorは変化しない。
Operation、parameter、type、field、caseのProtocol名は、finalized definition内にstableなstring dataとして保持する。JavaScript variable名、constructor名、functionの.name、prototype上のmethod列挙から推測することはしない。
Hostはincoming Operation名を、finalized dispatch table上のinternal tokenまたはaccessorへ解決する。Clientから受け取った名前をそのままobject property lookupに使うことはしない。Object-literal keyをauthoring sugarとして使う場合、property manglingからどう保護するかは未決定である。
Definition objectは、Hostへの登録時に一つのimmutableなartifactとしてfinalizeする。MutableなProtocol Interface参照を提供する場合は、同じ参照へ新しいvalidatorと新しいfinalized artifactを対応付ける。同じ参照とvalidatorの組から異なるDescriptorを返すことは認めない。
Operation implementation type
必要なOperation implementationのTypeScript型は、Interface definitionから推論する。
type FileOperations =
wip.Implementation<typeof FileInterface>;
概念的には、次のようなobject typeへ展開される。
type FileOperations = {
readonly read: (args: {
offset?: number;
length?: number;
}) => Promise<Uint8Array>;
readonly write: (args: {
content: Uint8Array;
}) => Promise<WriteResult>;
};
Function-valued propertyを基本形とし、strictFunctionTypesを含むstrictなcompiler settingsの下でparameterをcontravariantに検査できるようにする。通常のclass method parameterは、TypeScriptのassignabilityでbivariantに扱われる場合がある。そのため、method-shapedなimplementsだけを「すべてのschema-valid argumentを安全に受け取れる」ことの完全な証明にはしない。
Definition-awareなbuilderによって、implementationをcontextual typingする形も考えられる。
const operations = FileInterface.implement({
read: async (args) => {
// argsはruntime definitionから推論される
},
write: async (args) => {
// ...
},
});
implementを採用するかは未決定である。どの形でも、実行時の判定はruntime validationとcallable checkをauthorityとし、TypeScript型はdeveloper feedbackとして扱う。
上のfunction property shapeも説明用である。Operation parameterを一つのarguments objectとして渡すか、request contextを別parameterにするか、sync returnを許可するかは、まだ決めていない。
Object implementation
その時点でWorldspaceへ投影するObjectは、Host内部ではclassとして表現できる。
class File implements WipObject, FileOperations {
readonly #path: string;
readonly #objectMetadata: WipObjectMetadata;
constructor(
path: string,
objectMetadata: WipObjectMetadata,
) {
this.#path = path;
this.#objectMetadata = objectMetadata;
}
metadata(): WipObjectMetadata {
return this.#objectMetadata;
}
readonly read: FileOperations["read"] = async (args) => {
return filesystem.read(this.#path, args);
};
readonly write: FileOperations["write"] = async (args) => {
return filesystem.write(this.#path, args.content);
};
}
WipObjectとWipObjectMetadataは、Protocol Objectの公開metadataのうち、publication時に選ぶInterface集合以外を提供するための概念的な型である。具体的なshapeは未決定である。
interface WipObject {
metadata(): WipObjectMetadata;
}
interface WipObjectMetadata {
name: string;
description?: string;
ref?: string;
validator?: Uint8Array;
}
Classが保持する情報のすべてを公開metadataにするわけではない。次の情報はWipObjectMetadataに含めないが、implementing classのprivate fieldとして保持してよい。
- Absolute path。PathはObjectのfieldではなく、Worldspace resolutionのcontextである。
- Children。ChildrenはTree上の配置contextである。
- 固定されたInterface集合。InterfaceはObjectを公開する時点で追加する。
- Backend connection、file descriptor、credential等のHost-private state。
Classのpathやbackend handleはOperation implementationで使用できる。ただし、Object metadataやProtocol Objectへ自動的にserializeする対象にはしない。
また、classの使用は必須ではない。Structural typingを利用し、plain objectを使うこともできる。
const file = {
metadata() {
return objectMetadata;
},
read: async (args) => {
return filesystem.read(path, args);
},
write: async (args) => {
return filesystem.write(path, args.content);
},
} satisfies WipObject & FileOperations;
Object publication
ObjectをWorldspaceへ返す時点で、object instanceと、公開する0個以上のInterface definitionを明示的に組み合わせる。Interfaceは追加順を保持するordered setとし、同じdefinitionまたは同じ登録済み参照の重複追加は認めない。
const file = new File(path, objectMetadata);
const object = wip.publish(file)
.expose(FileInterface, file);
Publication APIは、概念的には各definitionと、そのImplementation型を満たすbindingを受け取る。
interface WipInterfaceDefinition<Implementation extends object> {
// runtime descriptor, codec, and dispatch artifact
}
interface PublishedObject<T extends WipObject> {
expose<Implementation extends object>(
definition: WipInterfaceDefinition<Implementation>,
implementation: Implementation,
): PublishedObject<T>;
}
実際のTypeScript generic表現は未決定である。このconstraintは、strictなcompiler settingsとfunction-valued property shapeを使用した場合に、各Interfaceへbindする値が必要なOperation functionを提供するかを検査するためのものである。
ImplementationにはObject instance自身を渡しても、Interface専用のadapter objectを渡してもよい。Interfaceを一つも追加しないObjectも表現できる。ただし、通常のclass methodやtype assertion等で型の検査を弱められるため、各Interfaceの追加時とdispatch時にはruntime callable checkを行い、Value validationも常に行う必要がある。
Host Runtimeは、各bindingを登録済みInterface参照と対応付ける。異なるInterfaceが同名Operationを宣言していても、dispatch対象は選択されたInterface参照に対応するbindingだけであり、単一object上の同名propertyへmergeしない。
PublishedObjectはHost SDK内のconceptual typeであり、Protocol typeには追加しない。Host Runtimeはこの値からProtocol Objectを構築する。
const protocolObject = {
name: metadata.name,
description: metadata.description,
interfaces: selectedInterfaceReferences,
ref: metadata.ref,
validator: metadata.validator,
};
Optional propertyのundefinedはwireへ出力せず、fieldの省略として扱う。nullへ自動変換することはしない。
selectedInterfaceReferencesには、Hostが各runtime Interface definitionの登録または公開時に割り当てる構造化参照{ scope, name }を、publication APIへの追加順で並べる。同じ(scope, name)の重複は拒否し、Objectの配置pathに対する祖先制約も検証する。Variable名やJavaScript object identityを、そのままportableなProtocol参照として使用することは要求しない。
observeでObjectに含めるのは、Descriptor自体ではなく、このInterface参照のordered setだけである。Clientが各参照をfetch_interfaceすると、Host Runtimeはruntime definition objectから構築したDescriptorと、存在する場合はInterface validatorを返す。
同じ参照とvalidatorの組に対応するDescriptorは、Host上で共有およびcacheできる。このため、Object instanceごとにDescriptorを再生成したり再送したりする必要はない。
Runtime definitionの責務
wip.interface(...)が返すdefinition objectには、Descriptorの構築に加えて、Valueの変換・検証とInterfaceごとのdispatchに必要な情報を提供する役割を想定する。
- Interface Descriptorを構築するruntime definition。
- Operation propertyとOperation Declarationの対応情報。
- ArgumentsのProtocol
Valuedecoder。 - ResultのProtocol
Valueencoder。 - Argumentsとresultのruntime validation。
- Operation名から、そのInterfaceへbindしたimplementationの対応functionを呼び出すdispatch code。
- 各Interfaceの推論されたImplementation型を満たす値を、そのInterface参照へbindするtyped entry point。
- HostがInterface参照、optional validator、Descriptorを登録、取得、cacheするためのlocal identity。
TypeScriptのstructural type checkはcompile-timeのdeveloper feedbackである。Runtimeでは、少なくとも次を検証する必要がある。
- Object metadataがProtocol上有効である。
- 追加する各InterfaceへbindしたOperation implementationが呼び出し可能である。
- 同じInterface参照をObjectへ重複して追加していない。
- ArgumentsがDescriptorに従っている。
- ResultがDescriptorに従っている。
- Integer、Number、Entry、Record、Union等の追加制約を満たす。
Generatedまたはgenericなdispatch codeは、TypeScript SDK内部の実装詳細である。Host内部のdispatch mechanism、class名、prototype、constructor等はProtocolへ公開しない。
TypeScript typeとProtocol type
Runtime definitionは、DescriptorのTypeExpr、Protocol Value、TypeScript static typeの三つを対応付ける。次の表はconceptualな対応例であり、具体的なpublic typeは未決定である。
| WIP definition | TypeScript representation | Runtime validation |
|---|---|---|
wip.unit() |
専用のWipUnit sentinel |
Optional省略と区別する |
wip.boolean() |
boolean |
Booleanのみ |
wip.integer() |
number |
Safe integer範囲 |
wip.number() |
number |
Finite binary64 |
wip.string() |
string |
Unicode string |
wip.bytes() |
Uint8Array等 |
Octet列 |
wip.json() |
JSON-compatible type | Bytesを拒否する |
wip.entry() |
Branded string等 | Canonical Worldspace path |
wip.record(...) |
Object type | Required、optional、unknown field |
wip.list(...) |
Arrayまたはreadonly array | 各itemを検証 |
wip.enum(...) |
String literal union | 宣言caseのみ |
wip.union(...) |
Discriminated union | Case、payload、shape |
wip.json()が受け付けるのは、null、boolean、finite number、string、Array、およびstring-keyedなplain objectから再帰的に構成されるJSON-compatible valueだけである。undefined、bigint、symbol、function、Uint8Array、Date、Map、Set、class instance、cyclic object graph、NaN、正負Infinity、sparse array等は、暗黙にJsonへ変換せずvalidation failureとする。Integerとして表現する値にも、Protocolの相互運用範囲を適用する。
TypeScriptのnumberはIntegerにもNumberにも使えるため、runtime schemaの位置に基づいて検証する。wip.integer()ではProtocolの相互運用範囲を超える値を拒否し、bigintを暗黙にIntegerへ変換しない。
Protocol Unitには、たとえばwip.unitValueのような専用singleton / sentinelを使い、undefinedは使わない。RequiredなUnit fieldではsentinelを明示する。OptionalなUnit fieldでは、sentinelがpresent Unitを表し、property欠落またはoptional positionのundefinedが省略を表す。
同様に、wip.optional(T)はfieldまたはparameterの省略を表すものであり、nullを許可する意味ではない。Implementationへ渡すarguments objectではproperty欠落を省略として扱う。Optional positionに明示したundefinedは、SDK convenienceとして省略へnormalizeしてよいが、undefined自体をProtocol Valueとしてencodeすることはしない。Required positionのundefinedはvalidation failureとする。
Dynamic Worldspaceでのlifecycle
observe
Projection implementationはrequest時にbackend dataを解決し、その時点のclassまたはplain object instanceを生成する。
async function resolveFile(
path: WipPath,
subject: Subject,
): Promise<PublishedObject | undefined> {
const entry = await filesystem.resolve(path);
if (!entry || !canSee(subject, entry)) {
return undefined;
}
const file = new File(
entry.path,
createObjectMetadata(entry),
);
let object = wip.publish(file)
.expose(ReadOnlyFileInterface, {
read: file.read,
});
if (canWrite(subject, entry)) {
object = object.expose(WritableFileInterface, {
write: file.write,
});
}
return object;
}
Host RuntimeはPublishedObjectからProtocol Objectを返す。Responseを生成した後は、このFile instanceを破棄してよい。Worldspace内の全fileに対応するinstanceを保持する必要はない。
Requested depthが正の場合、Providerはdirect childrenをlazyに発見できる。Directory全体を先に収集し、Arrayとして返すことは要求しない。ただし、一つのparentのobservationを成功させるには、公開されたindexable childrenをclean exhaustionまで列挙する必要がある。概念的には、Async Iterable等を利用できる。
const entries = filesystem.discoverChildren(path, budget);
for await (const entry of entries) {
const child = await projectCurrentObject(
entry.path,
context,
);
if (!child) {
continue;
}
observation.pushChild(child);
}
discoverChildren、budget、observationは具体APIではなく、conceptualな表記である。Childの発見と公開には、次のsemanticsを要求する。
- 各childを現在の主体についてprojectし、非公開Objectとそのsubtreeを返さない。
- 公開されたindexable childrenをすべて列挙し、clean exhaustionで終了する。
- Backend failure、cancellation、resource budget超過をclean exhaustionとして扱わず、部分的なsuccess responseを返さない。
- Requested depthに達したnodeではchild discoveryを行わず、
childrenを未観測として省略する。 - 異なるclassやplain objectを共通のpublished return typeとして扱える。
具体的なAsync Iterator contract、internal streaming、backpressure、error propagationは未決定である。
call_operation
Call時には現在のObjectを解決し直す。HostはrequestのTargetとInterfaceTargetをHost Runtimeへ渡し、Runtimeは一つのatomic execution boundaryに入ってから、target.pathを現在のObject implementationとstableなbackend対象へ解決する。
await runtime.callCurrent(
target,
interfaceTarget,
operation,
argumentsValue,
context,
(path, context) => resolveCurrentProjection(path, context),
);
resolveCurrentProjectionとruntime.callCurrentを実際のAPIとして採用するかは決めていない。ResolverはRuntimeが開始したexecution boundary内で呼ばれ、そのboundary中にObject identityとbackend対象を安定して保持するhandleまたはtransaction contextを含む値を返す必要がある。上のexampleは、次の要件を表す。
- 過去の
observeで作成したobject instanceを保持していなくてもOperationを実行できる。 - Call時の現在の主体、Object visibility、Object metadata、Interface集合を同じProjection pathで再解決し、Objectが非公開なら
NotFoundを返す。 - Requestの
interface.referenceが現在のObject.interfacesに含まれ、self-scopeを含む祖先制約を満たすことを確認する。指定されたinterface.scope_refと現在のscope Objectのrefも照合し、満たせない場合はInterfaceMismatchを返す。scope_refの省略だけでは拒否しない。 - 現在のInterfaceにOperationが宣言されていなければ
OperationNotFound、Operationを公開しつつ実行だけを拒否する場合はPermissionDeniedを返す。 interface.validatorを現在のInterface validatorへ照合する。Mutable InterfaceにvalidatorがなければInterfaceValidatorRequired、不一致ならInterfaceValidatorMismatchを返す。- 現在のProjected Object、Object validator、選択されたInterfaceとそのvalidator、Descriptor、Operation、そのInterfaceへbindしたTypeScript implementation、実際のbackend対象を一つのexecution boundary内で一致させる。
- Object validatorが指定されている場合は、現在のObject identityと保護対象状態の両方へ照合し、同じexecution boundary内でOperationを実行する。
- Pathが別Objectを指す場合は、provider-local revisionや内容が同じでもObject validatorを一致させない。
- Providerのvalidatorがobject-localなrevisionだけの場合、HostはObject identityを含む形へnamespace化またはwrapできなければならない。
- Object validatorが省略され、Operationがvalidatorを要求する場合は、実行せず
ValidatorRequiredを返す。 - Object validatorなしの実行をOperationが許可する場合は、実行時にpathが指す現在Objectへ無条件に実行する。
- Runtime dispatchが、選択されたInterface definitionへbindしたOperation implementationを現在のObject contextへ適用する。異なるInterfaceの同名Operationをmergeまたは曖昧に解決しない。
- Operation実行後の現在状態に対応するvalidatorを、必要に応じて
CallOperationResponseへ返せる。
Object validatorとObject identityの関連付けに、Client-facingなrefは必須ではない。ProviderまたはHostはHost-privateなidentityを使用できる。ただし、identity confirmation、Object validator比較、Interface membershipとvalidator比較、DescriptorとInterface-specific implementation bindingの固定、authorization、Operation実行は、一つのatomic boundaryで行う。
実行後のvalidatorをどこから取得するかも未決定である。同じobject instanceから再計算する方式、Operation resultとともに返す方式、backendを再観測する方式が考えられるが、いずれもresultを生成した実行後状態とvalidatorを対応させる必要がある。
Object instanceがpath文字列だけを保持している場合、validator比較後にpathを無条件で開き直すと、対象差し替えに対するatomicityを保証できないことがある。TypeScript APIには、transaction、conditional update、backend handle、lock等でbackend固有のatomicityを実現できることを求める。そのための抽象化は未決定である。
複数Interfaceの実装
一つのclassまたはplain objectは、複数のInterface implementation typeを満たせる。Property名が衝突する場合や、Interfaceごとに異なるbehaviorを提供する場合は、Interfaceごとのadapter objectをbindできる。
type ReadOnlyFileOperations =
wip.Implementation<typeof ReadOnlyFileInterface>;
type WritableFileOperations =
wip.Implementation<typeof WritableFileInterface>;
class File
implements
WipObject,
ReadOnlyFileOperations,
WritableFileOperations
{
// ...
}
現在の主体へ公開する0個以上のInterfaceはpublication時に選び、表示したい順に追加する。
let object = wip.publish(file)
.expose(ReadOnlyFileInterface, {
read: file.read,
});
if (authorization.canWrite) {
object = object.expose(WritableFileInterface, {
write: file.write,
});
}
return object;
Protocol Objectのinterfacesは追加順を保持するordered setであり、同じInterface参照の重複は拒否する。各Interface Descriptorとbindingは、それぞれ独立したOperation集合およびbehaviorを提供する。複数のImplementation bindingを一つのDescriptorやdispatch tableへ暗黙にmergeすることはしない。
異なるInterfaceが同じOperation名を宣言しても、dispatchはrequestのInterfaceTarget.referenceとOperation名の組で一意に決まる。そのInterfaceへbindしたimplementationを使うため、同じObjectに意味の異なる同名Operationを実装できる。
const object = wip.publish(file)
.expose(UserFileInterface, {
inspect: () => inspectForUser(file),
})
.expose(AuditFileInterface, {
inspect: () => inspectForAudit(file),
});
表示上のqualified表記はText Viewの参照表記に従い、signature全体をProtocol request fieldへparseし直さない。
Intersection type、interface extension、mixin等をDescriptorのcompositionへ自動変換することはしない。これらによって、Protocol上のInterface inheritanceを暗黙に導入することもない。
HostへのInterface登録
Runtime Interface definitionには、Hostへの登録を通じて構造化されたInterface参照を割り当てる必要がある。参照の絶対scopeとlocal name、および配置pathへの祖先制約は2.2. Operation and Interfaceに従う。
Self-scopeは許可され、scopeはそのpathのObjectに紐づく。Scope ObjectのoptionalなrefはProtocolのscope_refとして観測・検証する。refがなければidentityを追跡できないことも許容し、scopeの移動追跡や独立したscope validatorは導入しない。
登録方法としては、conceptualにはeager registrationとlazy registrationの両方が考えられる。次の例はscope / nameを渡す具体構文を省略しており、暗黙のroot scopeを定めてはいない。登録時のcontextと引数の具体APIも引き続き未決定である。
const readInterface = host.registerInterface(ReadOnlyFileInterface);
const writeInterface = host.registerInterface(WritableFileInterface);
const object = wip.publish(file)
.expose(readInterface, {
read: file.read,
})
.expose(writeInterface, {
write: file.write,
});
また、Hostが最初のpublication時に各definitionをinternする方式でもよい。どちらの登録方法でも、次の条件を満たす必要がある。
- MutableなInterface参照では
fetch_interfaceがvalidatorを返し、同じ参照とvalidatorの組から異なるDescriptorを返さない。 - Validatorを返さないInterface参照は、その参照の有効期間中Descriptorをimmutableにする。
- Descriptorを更新する場合は同じ参照へ新しいvalidatorを発行するか、新しいInterface参照を使用する。
- Objectの
interfacesへ設定した各Interface参照のDescriptorを、その主体からfetch_interfaceできる。 - 一つのObjectへ同じInterface参照を重複して追加しない。
- 構造的に同じDescriptorを同じ参照へdeduplicateすることは要求しない。
- Variable名、module path、JavaScript object address等を、そのままportableなProtocol semanticsとして扱わない。
- 一つのfinalized artifactを後からmutationしてDescriptorを変化させない。Hostはdefinitionをfreezeするか、検証済みのimmutable snapshotを保持できなければならない。
Registrationまたは最初のpublicationでは、Descriptor、type codec、validator、Operation dispatch tableを一つのfinalized artifactとして再帰的に検証し、固定する。fetch_interface、arguments / result validation、Operation dispatchは、すべて同じartifactを使う。
Descriptorだけをsnapshotし、dispatch時にmutation可能な元definitionを読み直す方式では、この固定を満たせない。Finalization後のmutationは拒否するか、既存artifactに影響しないものとして扱う。
Request contextとauthorization
Operation implementationが必要とする情報は、Object instanceだけとは限らない。Request contextとして、次の情報も必要になり得る。
- 現在の主体。
AbortSignal等のcancellation。- Deadline。
- Tracing / audit context。
- Host serviceやbackend connectionへのaccess。
- Resource limit。
このcontextをmethod parameter、wrapper object、AsyncLocalStorage、closure等のどれで渡すかは未決定である。どの形でも、Protocol parameterとして宣言された値とHostが注入するcontextは明確に区別する必要がある。
Interface集合はObject publication時に選択するだけでなく、Operation call時にもmembership、Interface validator、authorizationを再評価できなければならない。以前のobserveで公開したInterface集合だけを根拠に、現在のOperation実行を許可することはできない。
Resultのvisibility checkも、通常のstructural Value validationとは別に必要となる。Hostは、Operation resultのRecord、List、Union等にnestedしたすべてのEntryについて、現在の主体にそのpathを認識させてよいかを再帰的に確認できなければならない。この確認をRuntime definition、Projection、Host Runtimeのどこで行うかは未決定である。
Argumentとして受け取ったEntry pathについても、pathの所持をauthorizationの証明にはしない。参照先へ作用する時点で、現在の主体について認可する。
Error model
TypeScript methodのerror表現には、次の四種類を区別できることを求める。
- Protocol validationまたはdispatch failure。
- Effectがcommitされていないと保証できるHost / backend implementation failure。
- Operationの未実行またはrollbackを保証できない
OperationOutcomeUnknown。 - Interfaceがreturn typeとして宣言したdomain failure。
現在のProtocolはdomain failureをOperation resultのUnion caseとして表す。Thrown value、rejected Promise、explicit result objectをdomain resultまたはHost errorへどう対応付けるかは、Interface authoring APIで明示する必要がある。
Arbitrary thrown value、Promise rejection、result encode failure、宣言型に違反するimplementation resultを、domain failureへ自動変換することは認めない。Host Runtimeは、effectがcommitされていないと保証できる場合だけInternalへmappingし、保証できなければOperationOutcomeUnknownへmappingする。Abortやtransport切断は未実行の証明とはならず、自動retryはしない。
Custom transpilerを必須にしない
通常のTypeScript interfaceまたはtypeをsource of truthとしてInterface Descriptorを生成するには、TypeScript Compiler API、custom transformer、code generator等が必要になる。
Core APIでは、これらを必須にしない。Runtime definition objectをsource of truthとすることで、通常のtsc、bundler、JavaScript runtimeで動作できるようにする。
Build-time toolingは、optionalなauthoring layerとして追加してよい。
/** @wip-interface */
interface FileOperations {
read(args: ReadArguments): Promise<Uint8Array>;
}
Toolingによってこのdeclarationからruntime definition moduleを生成することはできる。ただし、その生成物もcore APIと同じruntime definition semanticsを満たす必要がある。
Build-time toolingには、少なくとも次の課題がある。
- TypeScript compiler versionとCompiler APIへの依存。
- Bundler、incremental build、package boundaryとの統合。
- Generic、conditional type、mapped type、importされたtypeの解決。
- Documentation抽出。
- Sourceとgenerated runtime schemaの一致。
- Runtime validation codeの生成。
Decoratorをcoreにしない
Decoratorだけでは通常、Union case、Record field、optional、Entry、Integer、nested type、generic、documentation等を、完全なruntime schemaとして取得できない。
emitDecoratorMetadata等が提供するconstructor-level metadataも、WIP Interface DescriptorとValue validationを再構築するには不足する。Decoratorをoptional sugarとして提供する場合には、明示的なruntime definitionまたはbuild-time generationへ接続する。
また、class inheritanceをInterface declarationの基本形にすると、複数Interface implementation、plain object、data-drivenなpublicationを不必要に制限することになる。このため、runtime definition objectをcore modelとする。
要件
このdraftのTypeScript API modelに求める要件は、少なくとも次のとおりである。
- Runtime definition objectをInterface Descriptorとruntime validationのsource of truthにする。
- 同じdefinitionからTypeScript implementation typeを推論できる。
- Strictなcompiler settingsとfunction-valued propertyを使用して、Object metadata providerが
WipObjectを満たし、各Interfaceへbindする値がそのImplementation型を満たすか検査できる。 - Compile-time型を完全なauthorityとせず、callable checkとProtocol boundaryのruntime validationを必須にする。
- 0個以上のInterfaceをpublication時に明示し、objectが持つpropertyやmethodの自動列挙に依存しない。
- Protocol上のOperation、parameter、type、field、case名をstableなstring dataとしてfinalizeし、constructor名やfunction
.nameから推測しない。 - Objectのpath、children、Interface集合をclassへ恒久的に固定しない。
- Classまたはplain object instanceをrequest scopeで生成および破棄できる。
- 全Object instanceの事前登録を要求しない。
- Direct childrenを現在の主体についてprojectし、非公開Objectとそのsubtreeを除外しつつlazyに発見し、success responseではclean exhaustionまで列挙する。
- Backend failure、cancellation、resource budget超過をclean exhaustionとして扱わず、部分的なobservationをsuccess responseとして返さない。
- 同じclassまたはplain objectへ複数のInterface-specific implementationを同時にbindでき、追加順を保持し、重複を拒否できる。
- 異なるInterfaceの同名Operationを、Interface参照とOperation名の組およびInterface-specific bindingでdispatchできる。
- Operation result内のnestedな
Entryへ、現在の主体についてvisibility checkを適用できる。 - Host-private fieldをProtocol ObjectまたはValueへ暗黙にserializeしない。
- Call時の現在の主体、Object visibility、Object validator、Interface membershipとvalidator、Descriptor、Operation、Interface-specific implementation binding、backend対象を一貫したexecution boundaryで扱う。
- 非公開Object、Interface非membership、非公開Operation、公開済みOperationの実行拒否をそれぞれ
NotFound、InterfaceMismatch、OperationNotFound、PermissionDeniedへ対応付ける。 - Interface validatorの必須・不一致を
InterfaceValidatorRequired、InterfaceValidatorMismatchへ対応付ける。 - Object validatorを現在のObject identityと保護対象状態へ結び付け、別Objectに同じprovider-local revisionがあっても一致させない。
- Object validator比較、identity confirmation、Interface membershipとvalidator比較、DescriptorとInterface-specific implementation bindingの固定、authorization、Operation実行を同じatomic boundaryで行う。
- Object validatorを要求するOperationへの省略を
ValidatorRequiredとして拒否できる。 - Provider、Host Runtime、runtime definition codeのerrorを、effect未commitを保証できる
Internalと、保証できないOperationOutcomeUnknownへ安全に対応付けられる。 - InvalidなObject、Descriptor、Operation resultをsuccess responseとして送信せず、Host-side validation failureをdomain failureへ変換しない。
- Custom transpiler、decorator、特定bundlerをHost実装の必須要件にしない。
- TypeScript SDK固有の型をProtocol semanticsとして要求しない。
非目標
次の事項は、このdraftでは決定しない。
- 最終的なpackage、module、function、generic type、class、methodの名前。
- 特定のschema libraryまたはvalidation libraryの採用。
- 特定のJavaScript runtimeまたはbundler。
- Object instanceの常駐registryまたはactor model。
- JavaScript object、prototype、constructorをwireへ公開すること。
- TypeScript interface extensionをProtocol Interface inheritanceとして扱うこと。
- Backend entityとTypeScript objectまたはProtocol Objectの一対一対応。
- TypeScript Compiler APIまたはcustom transformerの必須化。
- Client SDKのtyped proxy API。
未決定事項
WipObjectとWipObjectMetadataの具体的なshape。- Object metadata取得をsyncまたはasyncのどちらにするか。
- Publication APIがobjectを強参照、弱参照、closureのどれで保持するか。
wip.interface、wip.operation、type builderの最終syntax。wip.Inferとwip.Implementationの具体的なgeneric type。strictFunctionTypesを含むminimum TypeScript compiler settings。- Interface登録とscope / local name割当のlifecycle。
- 登録時にruntime definitionをdeep freezeするか、immutableなDescriptor / dispatch snapshotへ変換するか。
- Operation、parameter、type、field、caseのrename規則。
- Documentationのauthoring syntax。
- Runtime definitionと
TypeExprの対応。 readonlyproperty / arrayを許可するか。- Class method、function property、closureのどれをOperation implementationとして許可するか。
- Runtime dispatchがmethod receiverの
thisを保持する方法。 - Operation property名とProtocol operation名をminifierやproperty manglingからどう保護するか。
- Sync returnを許可してPromiseへnormalizeするか。
- Request contextとProtocol parameterのAPI上の分離。
- Nestedな
Entryresultのvisibility checkをRuntime definition、Projection、Host Runtimeのどこで行うか。 - Domain failure、thrown value、rejected Promise、Host errorの表現。
- Runtime dispatchとHost Runtimeの責務境界。
- Published ObjectをProviderから返す際のtype erasureまたは共通runtime representation。
- Validator比較とOperation実行を囲むbackend-specificなatomicity abstraction。
- Provider-local validatorをObject identityへnamespace化またはwrapするHost API。
- Operation実行後validatorをObject instance、Operation implementation、Providerのどこから取得するか。
- 主体ごとに異なるInterface combinationを追加したPublished Objectのstatic return typeをどう統一するか。
- TypeScript client APIを同じruntime definitionから生成するか。
- Build-time code generationをoptional packageとして提供するか。
これらの選択は、静的service objectに使えるだけでは足りない。Filesystem、database row、virtual aggregate、主体別Interface compositionも、同じAPIで実装できるかを評価して決定する。