# 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](../2-core-model.md)、[Object tree / Schema](../2.1-object-tree-schema.md)、[Operation and Interface](../2.2-operation-and-interface.md)、[Protocol](../3-protocol.md)に従う。動的なObject projectionの運用要件は[Dynamic Worldspace Projection](dynamic-worldspace-projection.md)、対応するRust modelは[Rust API Model](rust-api-model.md)を参照する。 ## 目的 TypeScript APIでは、runtimeに残るdefinition objectを使い、Protocol上の宣言とTypeScript型の推論を対応付けるmodelを自然に表現できるようにする。Objectの実装と、ある時点で公開するInterfaceの選択は分けて扱う。 1. Runtimeに残るdefinition objectでWIP Interfaceを宣言する。 2. 同じdefinition objectからInterface Descriptor、Value codec、validationを構築する。 3. Definition objectからOperation implementationのTypeScript型を推論する。 4. Classまたはplain objectが、Protocol Objectとして公開するmetadataと、必要に応じて0個以上のInterfaceのOperation implementationを提供する。 5. Objectを返す時点で、公開するInterfaceのordered setを明示的に選択する。 6. Object instanceをWorldspace全体について常駐させず、`observe`と`call_operation`の解決時にdata-drivenに生成できる。 7. 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を取得することはできない。 ```ts interface FileOperations { read(args: ReadArguments): Promise; } ``` ```ts host.registerInterface(FileOperations); // runtime valueではない ``` そこで、TypeScript APIではruntime valueをsource of truthにする。 ```ts const FileInterface = wip.interface({ // runtime schema }); ``` このobjectは、通常のJavaScript valueとしてtranspile後も残る。TypeScript型は、このruntime definitionから推論する。 ## Conceptual model ### Type definition Record、Enum、Union等の型も、runtime definition objectとして宣言する。 ```ts const WriteResult = wip.union({ name: "WriteResult", cases: { written: wip.record({ bytesWritten: wip.integer(), }), rejected: wip.record({ reason: wip.string(), }), }, }); ``` このdefinitionから、TypeScript型を推論する。 ```ts type WriteResult = wip.Infer; ``` `wip.Infer`の具体的な型実装とUnion DTOのTypeScript representationは、まだ決めていない。Runtime definitionと推論した型が、同じProtocol `TypeExpr`および`Value` semanticsを表すことを要件とする。 ### Interface definition WIP Interfaceも、runtime definition objectとして宣言する。 ```ts 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上の識別子であり、Protocol `InterfaceDescriptor`のfieldや構造化されたInterface参照としてそのまま公開しない。 - 各Operation definitionが保持するstableなstringをOperation Declarationの`name`として扱う。 - `parameters`で宣言されたstableなstringを名前付きOperation parameterとして扱う。 - `returns`をOperation return typeとして扱う。 - Runtime type definitionをDescriptorの`TypeExpr`とProtocol `Value`へ対応付ける。 - 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から推論する。 ```ts type FileOperations = wip.Implementation; ``` 概念的には、次のようなobject typeへ展開される。 ```ts type FileOperations = { readonly read: (args: { offset?: number; length?: number; }) => Promise; readonly write: (args: { content: Uint8Array; }) => Promise; }; ``` 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する形も考えられる。 ```ts 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として表現できる。 ```ts 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は未決定である。 ```ts 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を使うこともできる。 ```ts 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または同じ登録済み参照の重複追加は認めない。 ```ts const file = new File(path, objectMetadata); const object = wip.publish(file) .expose(FileInterface, file); ``` Publication APIは、概念的には各definitionと、そのImplementation型を満たすbindingを受け取る。 ```ts interface WipInterfaceDefinition { // runtime descriptor, codec, and dispatch artifact } interface PublishedObject { expose( definition: WipInterfaceDefinition, implementation: Implementation, ): PublishedObject; } ``` 実際の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を構築する。 ```ts 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 `Value` decoder。 - ResultのProtocol `Value` encoder。 - 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を生成する。 ```ts async function resolveFile( path: WipPath, subject: Subject, ): Promise { 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等を利用できる。 ```ts 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対象へ解決する。 ```ts 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できる。 ```ts type ReadOnlyFileOperations = wip.Implementation; type WritableFileOperations = wip.Implementation; class File implements WipObject, ReadOnlyFileOperations, WritableFileOperations { // ... } ``` 現在の主体へ公開する0個以上のInterfaceはpublication時に選び、表示したい順に追加する。 ```ts 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を実装できる。 ```ts const object = wip.publish(file) .expose(UserFileInterface, { inspect: () => inspectForUser(file), }) .expose(AuditFileInterface, { inspect: () => inspectForAudit(file), }); ``` 表示上のqualified表記は[Text Viewの参照表記](../4.2-text-view.md#参照表記)に従い、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](../2.2-operation-and-interface.md#interfaceの識別とscope)に従う。 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も引き続き未決定である。 ```ts 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として追加してよい。 ```ts /** @wip-interface */ interface FileOperations { read(args: ReadArguments): Promise; } ``` 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`の対応。 - `readonly` property / 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な`Entry` resultの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で実装できるかを評価して決定する。