Files

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の選択は分けて扱う。

  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を取得することはできない。

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上の識別子であり、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から推論する。

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 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を生成する。

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の対応。
  • 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で実装できるかを評価して決定する。