diff --git a/examples/blogger/reactive_service/src/blogger.service.ts b/examples/blogger/reactive_service/src/blogger.service.ts index 4e9c667a3..b7c554aa7 100644 --- a/examples/blogger/reactive_service/src/blogger.service.ts +++ b/examples/blogger/reactive_service/src/blogger.service.ts @@ -1,10 +1,10 @@ -import type { - Context, - EagerCollection, - Json, - Values, - Resource, - SkipService, +import { + type Context, + type EagerCollection, + type Json, + type Values, + type Resource, + type AnySkipService, } from "@skipruntime/core"; import { PostgresExternalService } from "@skip-adapter/postgres"; @@ -104,8 +104,8 @@ class PostsResource implements Resource { type PostsServiceInputs = Record; -export const service: SkipService = { - initialData: {}, +export const service: AnySkipService = { + inputs: {}, resources: { posts: PostsResource }, externalServices: { postgres }, createGraph( diff --git a/examples/cache_invalidation/edge_service/src/cache.service.ts b/examples/cache_invalidation/edge_service/src/cache.service.ts index a67050730..deb888ad0 100644 --- a/examples/cache_invalidation/edge_service/src/cache.service.ts +++ b/examples/cache_invalidation/edge_service/src/cache.service.ts @@ -7,13 +7,13 @@ * - Updates propagate to all connected clients in real-time */ -import type { - Context, - EagerCollection, - Json, - Values, - Resource, - SkipService, +import { + type Context, + type EagerCollection, + type Json, + type Values, + type Resource, + type AnySkipService, } from "@skipruntime/core"; import { PostgresExternalService } from "@skip-adapter/postgres"; @@ -119,8 +119,8 @@ class PostsResource implements Resource { type PostsServiceInputs = Record; -export const service: SkipService = { - initialData: {}, +export const service: AnySkipService = { + inputs: {}, resources: { posts: PostsResource }, externalServices: { postgres }, createGraph( diff --git a/examples/chatroom/reactive_service/src/chatroom.service.ts b/examples/chatroom/reactive_service/src/chatroom.service.ts index aea8497d6..27f48137d 100644 --- a/examples/chatroom/reactive_service/src/chatroom.service.ts +++ b/examples/chatroom/reactive_service/src/chatroom.service.ts @@ -1,11 +1,11 @@ -import type { - Context, - EagerCollection, - Json, - Mapper, - Resource, - SkipService, - Values, +import { + type Context, + type EagerCollection, + type Json, + type Mapper, + type Resource, + type AnySkipService, + type Values, } from "@skipruntime/core"; import { KafkaExternalService } from "@skip-adapter/kafka"; @@ -78,8 +78,8 @@ class MessagesResource implements Resource { } } -export const service: SkipService<{}, ResourceInputs> = { - initialData: {}, +export const service: AnySkipService = { + inputs: {}, resources: { messages: MessagesResource }, externalServices: { kafka }, createGraph(_: {}, context: Context): ResourceInputs { diff --git a/examples/hackernews/reactive_service/src/hackernews.service.ts b/examples/hackernews/reactive_service/src/hackernews.service.ts index d670cc310..9427f33ae 100644 --- a/examples/hackernews/reactive_service/src/hackernews.service.ts +++ b/examples/hackernews/reactive_service/src/hackernews.service.ts @@ -1,10 +1,11 @@ -import type { - Context, - EagerCollection, - Json, - Values, - Resource, - SkipService, +import { + InputDefinition, + type Context, + type EagerCollection, + type Json, + type Values, + type Resource, + type AnySkipService, } from "@skipruntime/core"; import { PostgresExternalService } from "@skip-adapter/postgres"; @@ -179,9 +180,9 @@ class SessionsResource implements Resource { * Main service definition * Configures resources, external services, and data flow */ -export const service: SkipService = { - initialData: { - sessions: [], +export const service: AnySkipService = { + inputs: { + sessions: new InputDefinition(), }, resources: { posts: PostsResource, sessions: SessionsResource }, externalServices: { postgres }, diff --git a/skipruntime-ts/addon/src/index.ts b/skipruntime-ts/addon/src/index.ts index 96597b88f..4cf9695a8 100644 --- a/skipruntime-ts/addon/src/index.ts +++ b/skipruntime-ts/addon/src/index.ts @@ -19,7 +19,7 @@ type AddOn = { const skip_runtime: AddOn = require("../build/Release/skip_runtime.node"); -import type { SkipService } from "@skipruntime/core"; +import type { AnySkipService } from "@skipruntime/core"; const jsonBinding: JsonBinding = skip_runtime.getJsonBinding(); const jsonConverter = buildJsonConverter(jsonBinding); @@ -31,7 +31,7 @@ const tobinding = new ToBinding( skip_runtime.getErrorObject, ); -export function initService(service: SkipService): Promise { +export function initService(service: AnySkipService): Promise { skip_runtime.initSkipRuntimeToBinding(tobinding); try { return Promise.resolve(tobinding.initService(service)); diff --git a/skipruntime-ts/core/src/api.ts b/skipruntime-ts/core/src/api.ts index c7476c5c3..1caf0e925 100644 --- a/skipruntime-ts/core/src/api.ts +++ b/skipruntime-ts/core/src/api.ts @@ -17,6 +17,30 @@ export type { Managed, Json, JsonObject, Opaque, DepSafe }; export { deepFreeze } from "../skiplang-json/index.js"; export type { Nullable }; +/** + * Type-erased base for eager collections, enabling sound typing in collection maps. + * + * Under sound subtyping, `EagerCollection` is not assignable to + * `EagerCollection` because `EagerCollection` uses `K` and `V` + * in both covariant and contravariant positions. This abstract base class + * erases the type parameters so that collections with different key/value + * types can be stored together in a `NamedEagerCollections` map. + * + * Uses a branded field to ensure nominal (not structural) typing. + */ +export abstract class AbstractEagerCollection { + readonly __sk_collectionBrand: undefined; +} + +/** + * Type-erased base for lazy collections, enabling sound typing in collection maps. + * + * Uses a branded field to ensure nominal (not structural) typing. + */ +export abstract class AbstractLazyCollection { + readonly __sk_lazyCollectionBrand: undefined; +} + /** * Reactive function that can be mapped over a collection. * @@ -58,7 +82,7 @@ export interface Reducer { /** * Initial accumulated value, providing the accumulated value for keys that are not associated to any values. */ - initial: Nullable; + initial: A | null; /** * Include a new value into the accumulated value. @@ -67,7 +91,7 @@ export interface Reducer { * @param value - The added value. * @returns The updated accumulated value. */ - add(accum: Nullable, value: V & DepSafe): A; + add(accum: A | null, value: V & DepSafe): A; /** * Exclude a previously added value from the accumulated value. @@ -81,7 +105,7 @@ export interface Reducer { * @param value - The removed value. * @returns The updated accumulated value, or `null` indicating that the accumulated value should be recomputed using `add` and `initial`. */ - remove(accum: A, value: V & DepSafe): Nullable; + remove(accum: A, value: V & DepSafe): A | null; } /** @@ -112,7 +136,8 @@ export interface Values extends Iterable { * @typeParam V - Type of values. */ export interface LazyCollection - extends Managed { + extends AbstractLazyCollection, + Managed { /** * Get (and potentially compute) all values associated to `key`. * @param key - The key to query. @@ -144,7 +169,8 @@ export interface LazyCollection * @typeParam V - Type of values. */ export interface EagerCollection - extends Managed { + extends AbstractEagerCollection, + Managed { /** * Get all values associated to a key. * @@ -190,7 +216,7 @@ export interface EagerCollection * @param params - Additional parameters to `mapper`. * @returns The resulting eager collection. */ - map( + map( mapper: new (...params: Params) => Mapper, ...params: Params ): EagerCollection; @@ -219,7 +245,7 @@ export interface EagerCollection * @param params - Additional parameters to `reducer` * @returns The resulting eager collection. */ - reduce( + reduce( reducer: new (...params: Params) => Reducer, ...params: Params ): EagerCollection; @@ -237,7 +263,11 @@ export interface EagerCollection * @param mapper - Constructor of `Mapper` class to transform each entry of this collection. * @param mapperParams - Additional parameters to `mapper`. */ - mapReduce( + mapReduce< + K2 extends Json, + V2 extends Json, + MapperParams extends readonly DepSafe[], + >( mapper: new (...params: MapperParams) => Mapper, ...mapperParams: MapperParams ): // @@ -248,7 +278,7 @@ export interface EagerCollection * @param reducerParams - Additional parameters to `reducer` * @returns The resulting eager collection. */ - ( + ( reducer: new (...params: ReducerParams) => Reducer, ...reducerParams: ReducerParams ) => EagerCollection; @@ -340,7 +370,7 @@ export interface Context { createLazyCollection< K extends Json, V extends Json, - Params extends DepSafe[], + Params extends readonly DepSafe[], >( compute: new (...params: Params) => LazyCompute, ...params: Params @@ -457,9 +487,20 @@ export interface ExternalService { /** * Association of names to collections. + * @deprecated Use `NamedEagerCollections` instead for sound typing. */ export type NamedCollections = { [name: string]: EagerCollection }; +/** + * Association of names to type-erased eager collections. + * + * Uses `readonly` fields to enable covariant checking under sound subtyping, + * allowing concrete collection maps to be assignable to this type. + */ +export type NamedEagerCollections = { + readonly [name: string]: AbstractEagerCollection; +}; + /** * Resource provided by a `SkipService`. * @@ -467,9 +508,7 @@ export type NamedCollections = { [name: string]: EagerCollection }; * * @typeParam Collections - Collections provided to the resource computation by the service's `createGraph`. */ -export interface Resource< - Collections extends NamedCollections = NamedCollections, -> { +export interface Resource { /** * Build the reactive compute graph of the reactive resource. * @@ -480,15 +519,30 @@ export interface Resource< instantiate( collections: Collections, context: Context, - ): EagerCollection; + ): AbstractEagerCollection; } +/** + * Constructor type for a `Resource`, enabling sound typing of resource constructor parameters. + * + * Using `never` for the `Params` position leverages function parameter contravariance: + * `new (params: SpecificType) => R` is assignable to `new (params: never) => R`. + * + * @typeParam Collections - Collections provided to the resource computation. + * @typeParam Params - Type of the constructor parameter. + */ +export type ResourceClass< + Collections extends NamedEagerCollections, + Params extends Json, +> = new (params: Params) => Resource; + /** * Initial data for a service's input collections. * * The initial data to populate a service's input collections is provided as an association from collection names to arrays of entries. * * @typeParam Inputs - Collections provided to the service's `createGraph`. + * @deprecated Use `InputDefinition` and `NamedInputDefinitions` instead. */ export type InitialData = { [Name in keyof Inputs]: Inputs[Name] extends EagerCollection @@ -496,6 +550,39 @@ export type InitialData = { : Entry[]; }; +/** + * Type-erased base for input definitions, enabling sound typing in input maps. + * + * Uses a branded field to ensure nominal (not structural) typing. + */ +export abstract class AbstractInputDefinition { + readonly __sk_inputDefBrand: undefined; +} + +/** + * Definition of an input collection, including its initial data. + * + * @typeParam K - Type of keys. + * @typeParam V - Type of values. + */ +export class InputDefinition< + K extends Json, + V extends Json, +> extends AbstractInputDefinition { + readonly initial: Entry[]; + constructor(initial: Entry[] = []) { + super(); + this.initial = initial; + } +} + +/** + * Association of names to type-erased input definitions. + */ +export type NamedInputDefinitions = { + readonly [name: string]: AbstractInputDefinition; +}; + /** * A Skip reactive service encapsulating a reactive computation. * @@ -542,22 +629,23 @@ export type InitialData = { * @typeParam ResourceInputs - Collections provided to the resource computation by the service's `createGraph`. */ export interface SkipService< - Inputs extends NamedCollections = NamedCollections, - ResourceInputs extends NamedCollections = NamedCollections, + InputDefs extends NamedInputDefinitions, + Inputs extends NamedEagerCollections, + ResourceInputs extends NamedEagerCollections, > { /** - * Initial data for this service's input collections. + * Input definitions for this service's input collections, including initial data. * - * @remarks While the initial data is not required to have a `DepSafe` type (only a subtype of `Json` is required); note that any modifications made to any objects passed as `initialData` will *not* be seen by a service once started. + * @remarks While the initial data is not required to have a `DepSafe` type (only a subtype of `Json` is required); note that any modifications made to any objects passed as initial data will *not* be seen by a service once started. */ - initialData?: InitialData; + readonly inputs: InputDefs; /** External services that may be used by this service's reactive computation. */ - externalServices?: { [name: string]: ExternalService }; + readonly externalServices?: { readonly [name: string]: ExternalService }; /** Reactive resources which constitute the public interface of this reactive service. */ - resources: { - [name: string]: new (params: Json) => Resource; + readonly resources: { + readonly [name: string]: ResourceClass; }; /** @@ -569,3 +657,13 @@ export interface SkipService< */ createGraph(inputCollections: Inputs, context: Context): ResourceInputs; } + +/** + * Type-erased SkipService, for use in internal runtime code and consumers + * that don't need specific type parameters. + */ +export type AnySkipService = SkipService< + NamedInputDefinitions, + NamedEagerCollections, + NamedEagerCollections +>; diff --git a/skipruntime-ts/core/src/binding.ts b/skipruntime-ts/core/src/binding.ts index 5cf107793..6096b11c2 100644 --- a/skipruntime-ts/core/src/binding.ts +++ b/skipruntime-ts/core/src/binding.ts @@ -6,6 +6,7 @@ import { type ExternalService, type LazyCompute, type Mapper, + type NamedEagerCollections, type Reducer, type Resource, } from "./api.js"; @@ -14,9 +15,13 @@ import type { HandlerInfo, ServiceDefinition } from "./index.js"; export type Handle = Internal.Opaque; export class ResourceBuilder { - constructor(private readonly builder: new (params: Json) => Resource) {} + constructor( + private readonly builder: new ( + params: Json, + ) => Resource, + ) {} - build(parameters: Json): Resource { + build(parameters: Json): Resource { const builder = this.builder; return new builder(parameters); } @@ -70,7 +75,9 @@ export interface FromBinding { // Resource - SkipRuntime_createResource(ref: Handle): Pointer; + SkipRuntime_createResource( + ref: Handle>, + ): Pointer; // Service diff --git a/skipruntime-ts/core/src/index.ts b/skipruntime-ts/core/src/index.ts index ee67702bc..11a12bc63 100644 --- a/skipruntime-ts/core/src/index.ts +++ b/skipruntime-ts/core/src/index.ts @@ -22,6 +22,7 @@ import { sknative } from "../skiplang-std/index.js"; import type * as Internal from "./internal.js"; import { + type AbstractEagerCollection, type CollectionUpdate, type Context, type EagerCollection, @@ -29,14 +30,15 @@ import { type LazyCollection, type LazyCompute, type Mapper, - type NamedCollections, + type NamedEagerCollections, type Values, type DepSafe, type Reducer, type Resource, - type SkipService, + type AnySkipService, type Watermark, type ExternalService, + InputDefinition, } from "./api.js"; import { @@ -58,15 +60,18 @@ export type JSONOperator = JSONMapper | JSONLazyCompute | Reducer; export type HandlerInfo

= { object: P; name: string; - params: DepSafe[]; + params: readonly DepSafe[]; }; -function instantiateUserObject( +function instantiateUserObject< + Params extends readonly DepSafe[], + Result extends object, +>( what: string, ctor: new (...params: Params) => Result, params: Params, ): HandlerInfo { - const checkedParams = params.map(checkOrCloneParam) as Params; + const checkedParams = params.map(checkOrCloneParam) as unknown as Params; const obj = new ctor(...checkedParams); Object.freeze(obj); if (!obj.constructor.name) { @@ -98,38 +103,47 @@ export interface ChangeManager { export class ServiceDefinition { constructor( - private service: SkipService, + private service: AnySkipService, private readonly externals: Map = new Map(), ) {} - buildResource(name: string, parameters: Json): Resource { - const builder = this.service.resources[name]; + buildResource( + name: string, + parameters: Json, + ): Resource { + const builder = ( + this.service.resources as { + readonly [name: string]: new ( + params: Json, + ) => Resource; + } + )[name]; if (!builder) throw new Error(`Resource '${name}' not exist.`); return new builder(parameters); } inputs(): string[] { - return this.service.initialData - ? Object.keys(this.service.initialData) - : []; + return Object.keys(this.service.inputs); } resources(): string[] { - return Object.keys(this.service.resources); + return Object.keys(this.service.resources as object); } initialData(name: string): Entry[] { - if (!this.service.initialData) throw new Error(`No initial data defined.`); - const data = this.service.initialData[name]; - if (!data) throw new Error(`Initial data '${name}' not exist.`); - return data; + const inputDef = this.service.inputs[name]; + if (!inputDef) throw new Error(`Input definition '${name}' not exist.`); + return (inputDef as InputDefinition).initial; } createGraph( - inputCollections: NamedCollections, + inputCollections: NamedEagerCollections, context: Context, - ): NamedCollections { - return this.service.createGraph(inputCollections, context); + ): NamedEagerCollections { + return this.service.createGraph( + inputCollections, + context, + ) as NamedEagerCollections; } subscribe( @@ -183,7 +197,7 @@ export class ServiceDefinition { await Promise.all(promises); } - derive(service: SkipService): ServiceDefinition { + derive(service: AnySkipService): ServiceDefinition { return new ServiceDefinition(service, new Map(this.externals)); } } @@ -238,6 +252,8 @@ class LazyCollectionImpl extends SkManaged implements LazyCollection { + readonly __sk_lazyCollectionBrand: undefined; + constructor( readonly lazyCollection: string, private readonly refs: ToBinding, @@ -276,6 +292,8 @@ class EagerCollectionImpl extends SkManaged implements EagerCollection { + readonly __sk_collectionBrand: undefined; + constructor( public readonly collection: string, private readonly refs: ToBinding, @@ -335,7 +353,7 @@ class EagerCollectionImpl return this.derive(skcollection); } - map( + map( mapper: new (...params: Params) => Mapper, ...params: Params ): EagerCollection { @@ -350,11 +368,15 @@ class EagerCollectionImpl return this.derive(mapped); } - mapReduce( + mapReduce< + K2 extends Json, + V2 extends Json, + MapperParams extends readonly DepSafe[], + >( mapper: new (...params: MapperParams) => Mapper, ...mapperParams: MapperParams ) { - return ( + return ( reducer: new (...params: ReducerParams) => Reducer, ...reducerParams: ReducerParams ) => { @@ -395,7 +417,7 @@ class EagerCollectionImpl }; } - reduce( + reduce( reducer: new (...params: Params) => Reducer, ...params: Params ): EagerCollection { @@ -519,7 +541,7 @@ class ContextImpl implements Context { createLazyCollection< K extends Json, V extends Json, - Params extends DepSafe[], + Params extends readonly DepSafe[], >( compute: new (...params: Params) => LazyCompute, ...params: Params @@ -559,9 +581,9 @@ class ContextImpl implements Context { } export class ServiceInstanceFactory { - constructor(private init: (service: SkipService) => ServiceInstance) {} + constructor(private init: (service: AnySkipService) => ServiceInstance) {} - initService(service: SkipService): ServiceInstance { + initService(service: AnySkipService): ServiceInstance { return this.init(service); } } @@ -805,7 +827,7 @@ export class ServiceInstance { } } - async reload(service: SkipService, changes: ChangeManager): Promise { + async reload(service: AnySkipService, changes: ChangeManager): Promise { if (this.forkName) { throw new SkipError("Reload cannot be called in transaction."); } @@ -1099,12 +1121,12 @@ export class ToBinding { // Resource SkipRuntime_Resource__instantiate( - skresource: Handle, + skresource: Handle>, skcollections: Pointer, ): string { const skjson = this.getJsonConverter(); const resource = this.handles.get(skresource); - const collections: NamedCollections = {}; + const collections: { [key: string]: AbstractEagerCollection } = {}; const keysIds = skjson.importJSON(skcollections) as { [key: string]: string; }; @@ -1112,10 +1134,14 @@ export class ToBinding { collections[key] = new EagerCollectionImpl(name, this); } const collection = resource.instantiate(collections, new ContextImpl(this)); - return EagerCollectionImpl.getName(collection); + return EagerCollectionImpl.getName( + collection as EagerCollection, + ); } - SkipRuntime_deleteResource(resource: Handle): void { + SkipRuntime_deleteResource( + resource: Handle>, + ): void { this.handles.deleteHandle(resource); } @@ -1127,7 +1153,7 @@ export class ToBinding { ): Pointer { const skjson = this.getJsonConverter(); const service = this.handles.get(skservice); - const collections: NamedCollections = {}; + const collections: { [key: string]: AbstractEagerCollection } = {}; const keysIds = skjson.importJSON(skcollections) as { [key: string]: string; }; @@ -1137,7 +1163,9 @@ export class ToBinding { const result = service.createGraph(collections, new ContextImpl(this)); const collectionsNames: { [name: string]: string } = {}; for (const [name, collection] of Object.entries(result)) { - collectionsNames[name] = EagerCollectionImpl.getName(collection); + collectionsNames[name] = EagerCollectionImpl.getName( + collection as EagerCollection, + ); } return skjson.exportJSON(collectionsNames); } @@ -1349,7 +1377,7 @@ export class ToBinding { this.handles.deleteHandle(reducer); } - async initService(service: SkipService): Promise { + async initService(service: AnySkipService): Promise { this.setFork(null); const uuid = crypto.randomUUID(); this.fork(uuid); diff --git a/skipruntime-ts/examples/database.ts b/skipruntime-ts/examples/database.ts index 7386fecc0..d85c90b90 100644 --- a/skipruntime-ts/examples/database.ts +++ b/skipruntime-ts/examples/database.ts @@ -1,9 +1,10 @@ import type { EagerCollection, - SkipService, + AnySkipService, Resource, Entry, } from "@skipruntime/core"; +import { InputDefinition } from "@skipruntime/core"; import { runService } from "@skipruntime/server"; @@ -61,11 +62,9 @@ class UsersResource implements Resource { // Setting up the service /*****************************************************************************/ -function serviceWithInitialData( - users: Entry[], -): SkipService { +function serviceWithInitialData(users: Entry[]): AnySkipService { return { - initialData: { users }, + inputs: { users: new InputDefinition(users) }, resources: { users: UsersResource }, createGraph: (inputCollections) => inputCollections, }; diff --git a/skipruntime-ts/examples/departures.ts b/skipruntime-ts/examples/departures.ts index 372f63d4c..61a770a7e 100644 --- a/skipruntime-ts/examples/departures.ts +++ b/skipruntime-ts/examples/departures.ts @@ -3,8 +3,9 @@ import type { EagerCollection, Json, Resource, - SkipService, + AnySkipService, } from "@skipruntime/core"; +import { InputDefinition } from "@skipruntime/core"; import { runService } from "@skipruntime/server"; import { PolledExternalService } from "@skipruntime/helpers"; @@ -51,8 +52,8 @@ class DeparturesResource implements Resource { } } -const service: SkipService = { - initialData: { config: [] }, +const service: AnySkipService = { + inputs: { config: new InputDefinition() }, resources: { departures: DeparturesResource, }, diff --git a/skipruntime-ts/examples/groups.ts b/skipruntime-ts/examples/groups.ts index 4ac25ba92..61906c776 100644 --- a/skipruntime-ts/examples/groups.ts +++ b/skipruntime-ts/examples/groups.ts @@ -1,10 +1,10 @@ import { type EagerCollection, - type InitialData, type Json, type Mapper, type Resource, type Values, + InputDefinition, } from "@skipruntime/core"; import { runService } from "@skipruntime/server"; @@ -90,22 +90,22 @@ class ActiveFriends implements Resource { } // Load initial data from a source-of-truth database (mocked for simplicity) -const initialData: InitialData = { - users: [ +const inputs = { + users: new InputDefinition([ [0, [{ name: "Bob", active: true, friends: [1, 2] }]], [1, [{ name: "Alice", active: true, friends: [0, 2] }]], [2, [{ name: "Carol", active: false, friends: [0, 1] }]], [3, [{ name: "Eve", active: true, friends: [] }]], - ], - groups: [ + ]), + groups: new InputDefinition([ [1001, [{ name: "Group 1", members: [1, 2, 3] }]], [1002, [{ name: "Group 2", members: [0, 2] }]], - ], + ]), }; // Specify and run the reactive service const service = { - initialData, + inputs, resources: { active_friends: ActiveFriends }, createGraph(input: ServiceInputs): ResourceInputs { const users = input.users; diff --git a/skipruntime-ts/examples/remote.ts b/skipruntime-ts/examples/remote.ts index b556e9918..ac71113ee 100644 --- a/skipruntime-ts/examples/remote.ts +++ b/skipruntime-ts/examples/remote.ts @@ -2,7 +2,7 @@ import type { Context, EagerCollection, Mapper, - NamedCollections, + NamedEagerCollections, Resource, Values, } from "@skipruntime/core"; @@ -33,9 +33,9 @@ class Mult implements Mapper { } } -class MultResource implements Resource { +class MultResource implements Resource { instantiate( - _collections: NamedCollections, + _collections: NamedEagerCollections, context: Context, ): EagerCollection { const sub = context @@ -54,6 +54,7 @@ class MultResource implements Resource { } } const service = { + inputs: {}, resources: { data: MultResource }, externalServices: { sumexample: SkipExternalService.direct({ @@ -63,7 +64,7 @@ const service = { }), }, - createGraph(inputCollections: NamedCollections) { + createGraph(inputCollections: NamedEagerCollections) { return inputCollections; }, }; diff --git a/skipruntime-ts/examples/sheet.ts b/skipruntime-ts/examples/sheet.ts index 9836b3c02..fdd6ef7c0 100644 --- a/skipruntime-ts/examples/sheet.ts +++ b/skipruntime-ts/examples/sheet.ts @@ -7,6 +7,7 @@ import type { Resource, Values, } from "@skipruntime/core"; +import { InputDefinition } from "@skipruntime/core"; import { runService } from "@skipruntime/server"; @@ -84,7 +85,7 @@ class ComputedCells implements Resource { } } const service = { - initialData: { cells: [] }, + inputs: { cells: new InputDefinition() }, resources: { computed: ComputedCells }, createGraph(inputCollections: Inputs, context: Context): Outputs { const cells = inputCollections.cells; diff --git a/skipruntime-ts/examples/sum.ts b/skipruntime-ts/examples/sum.ts index e91b579fb..678f072a4 100644 --- a/skipruntime-ts/examples/sum.ts +++ b/skipruntime-ts/examples/sum.ts @@ -4,6 +4,7 @@ import type { Resource, Values, } from "@skipruntime/core"; +import { InputDefinition } from "@skipruntime/core"; import { runService } from "@skipruntime/server"; @@ -60,7 +61,7 @@ class Sub implements Resource { } const service = { - initialData: { input1: [], input2: [] }, + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { add: Add, sub: Sub }, createGraph: (inputs: Collections) => inputs, }; diff --git a/skipruntime-ts/helpers/src/remote.ts b/skipruntime-ts/helpers/src/remote.ts index 1cc99c7a7..d6bf32e7f 100644 --- a/skipruntime-ts/helpers/src/remote.ts +++ b/skipruntime-ts/helpers/src/remote.ts @@ -3,14 +3,15 @@ import EventSource from "eventsource"; import type { + AbstractEagerCollection, + AnySkipService, Context, EagerCollection, Entry, ExternalService, Json, - NamedCollections, + NamedEagerCollections, Resource, - SkipService, } from "@skipruntime/core"; import { SkipError } from "@skipruntime/core"; @@ -111,7 +112,7 @@ export class SkipExternalService implements ExternalService { } } -class LeaderResource implements Resource { +class LeaderResource implements Resource { private collection: string; constructor(param: Json) { @@ -122,7 +123,7 @@ class LeaderResource implements Resource { ); } - instantiate(collections: NamedCollections): EagerCollection { + instantiate(collections: NamedEagerCollections): AbstractEagerCollection { if (this.collection in collections) return collections[this.collection]!; throw new SkipError( `Unknown shared collection in leader: ${this.collection}`, @@ -137,7 +138,7 @@ class LeaderResource implements Resource { * * @returns The *leader* component to run `service` in such a configuration. */ -export function asLeader(service: SkipService): SkipService { +export function asLeader(service: AnySkipService): AnySkipService { //TODO: add mechanism to split externals between leader/follower return { ...service, @@ -153,21 +154,26 @@ export function asLeader(service: SkipService): SkipService { * @returns The *follower* component to run `service` in such a configuration, given the leader's address and the names of the shared computation graph collections to be mirrored from it (typically the `ResourceInputs` of `service`). */ export function asFollower( - service: SkipService, + service: AnySkipService, leader: { leader: { host: string; streaming_port: number; control_port: number }; collections: string[]; }, -): SkipService { +): AnySkipService { return { ...service, - initialData: {}, + inputs: {}, externalServices: { ...service.externalServices, __skip_leader: SkipExternalService.direct(leader.leader), }, - createGraph(_inputs: object, context: Context): NamedCollections { - const mirroredCollections: NamedCollections = {}; + createGraph( + _inputs: NamedEagerCollections, + context: Context, + ): NamedEagerCollections { + const mirroredCollections: { + [key: string]: EagerCollection; + } = {}; for (const collection of leader.collections) { mirroredCollections[collection] = context.useExternalResource({ service: "__skip_leader", diff --git a/skipruntime-ts/server/src/server.ts b/skipruntime-ts/server/src/server.ts index 8ca326e92..e12e5644a 100644 --- a/skipruntime-ts/server/src/server.ts +++ b/skipruntime-ts/server/src/server.ts @@ -4,7 +4,7 @@ * @packageDocumentation */ -import type { SkipService } from "@skipruntime/core"; +import type { AnySkipService } from "@skipruntime/core"; import { registerControlServiceRoutes, registerStreamingServiceRoutes, @@ -103,7 +103,7 @@ export type SkipServer = { * @returns Object to manage the running server. */ export async function runService( - service: SkipService, + service: AnySkipService, options: { streaming_port: number; control_port: number; diff --git a/skipruntime-ts/server/test/no_cors.spec.ts b/skipruntime-ts/server/test/no_cors.spec.ts index 3894f07d4..d1ccdbe37 100644 --- a/skipruntime-ts/server/test/no_cors.spec.ts +++ b/skipruntime-ts/server/test/no_cors.spec.ts @@ -1,5 +1,6 @@ import { runService, type SkipServer } from "../src/server.js"; import type { Context, EagerCollection, Resource } from "@skipruntime/core"; +import { InputDefinition } from "@skipruntime/core"; import { expect } from "chai"; type Post = { @@ -25,7 +26,7 @@ describe("runService({ no_cors: true })", function () { before(async function () { service = await runService( { - initialData: { posts: [] }, + inputs: { posts: new InputDefinition() }, resources: { posts: PostsResource }, createGraph( inputs: { posts: EagerCollection }, diff --git a/skipruntime-ts/tests/native_addon/test.ts b/skipruntime-ts/tests/native_addon/test.ts index 1fbde6b89..69f00bfb8 100644 --- a/skipruntime-ts/tests/native_addon/test.ts +++ b/skipruntime-ts/tests/native_addon/test.ts @@ -1,8 +1,8 @@ -import type { SkipService } from "@skipruntime/core"; +import type { AnySkipService } from "@skipruntime/core"; import { initService } from "@skipruntime/native"; -const emptyService: SkipService = { - initialData: {}, +const emptyService: AnySkipService = { + inputs: {}, resources: {}, createGraph(inputCollections) { return inputCollections; diff --git a/skipruntime-ts/tests/native_addon_unreleased/test.ts b/skipruntime-ts/tests/native_addon_unreleased/test.ts index 1fbde6b89..69f00bfb8 100644 --- a/skipruntime-ts/tests/native_addon_unreleased/test.ts +++ b/skipruntime-ts/tests/native_addon_unreleased/test.ts @@ -1,8 +1,8 @@ -import type { SkipService } from "@skipruntime/core"; +import type { AnySkipService } from "@skipruntime/core"; import { initService } from "@skipruntime/native"; -const emptyService: SkipService = { - initialData: {}, +const emptyService: AnySkipService = { + inputs: {}, resources: {}, createGraph(inputCollections) { return inputCollections; diff --git a/skipruntime-ts/tests/src/tests.ts b/skipruntime-ts/tests/src/tests.ts index f797c8513..490070ebd 100644 --- a/skipruntime-ts/tests/src/tests.ts +++ b/skipruntime-ts/tests/src/tests.ts @@ -8,19 +8,19 @@ import type { LazyCompute, LazyCollection, Values, - SkipService, + AnySkipService, Resource, Entry, ExternalService, ServiceInstance, CollectionUpdate, - NamedCollections, + NamedEagerCollections, SubscriptionID, Nullable, Reducer, ChangeManager, } from "@skipruntime/core"; -import { LoadStatus } from "@skipruntime/core"; +import { LoadStatus, InputDefinition } from "@skipruntime/core"; import { Count, Sum } from "@skipruntime/helpers"; import { it as mit, type AsyncFunc } from "mocha"; @@ -181,8 +181,8 @@ class Map1Resource implements Resource { } } -const map1Service: SkipService = { - initialData: { input: [] }, +const map1Service: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { map1: Map1Resource }, createGraph(inputCollections: Input_SN) { @@ -218,8 +218,8 @@ class Map2Resource implements Resource { } } -const map2Service: SkipService = { - initialData: { input1: [], input2: [] }, +const map2Service: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { map2: Map2Resource }, createGraph(inputCollections: Input_SN_SN) { @@ -241,8 +241,8 @@ class Map3Resource implements Resource { } } -const map3Service: SkipService = { - initialData: { input1: [], input2: [] }, +const map3Service: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { map3: Map3Resource }, createGraph(inputCollections: Input_SN_SN) { @@ -272,8 +272,8 @@ class OneToOneMapperResource implements Resource { } } -const oneToOneMapperService: SkipService = { - initialData: { input: [] }, +const oneToOneMapperService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { valueMapper: OneToOneMapperResource }, createGraph(inputCollections: Input_NN) { @@ -302,8 +302,8 @@ class SizeResource implements Resource { } } -const sizeService: SkipService = { - initialData: { input1: [], input2: [] }, +const sizeService: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { size: SizeResource }, createGraph(inputCollections: Input_NN_NN) { @@ -324,8 +324,8 @@ class SlicedMap1Resource implements Resource { } } -const slicedMap1Service: SkipService = { - initialData: { input: [] }, +const slicedMap1Service: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { slice: SlicedMap1Resource }, createGraph(inputCollections: Input_NN) { @@ -361,8 +361,8 @@ class LazyResource implements Resource { } } -const lazyService: SkipService = { - initialData: { input: [] }, +const lazyService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { lazy: LazyResource }, createGraph(inputCollections: Input_NN) { @@ -384,8 +384,8 @@ class MapReduceResource implements Resource { } } -const mapReduceService: SkipService = { - initialData: { input: [] }, +const mapReduceService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { mapReduce: MapReduceResource }, createGraph(inputCollections: Input_NN) { @@ -412,8 +412,8 @@ class UserMapReduceResource implements Resource { } } -const userMapReduceService: SkipService = { - initialData: { input: [] }, +const userMapReduceService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { userMapReduce: UserMapReduceResource }, createGraph(inputCollections: Input_NN) { @@ -429,8 +429,8 @@ class CountResource implements Resource { } } -const countService: SkipService = { - initialData: { input: [] }, +const countService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { count: CountResource }, createGraph(inputCollections: Input_NN) { @@ -446,8 +446,8 @@ class Merge1Resource implements Resource { } } -const merge1Service: SkipService = { - initialData: { input1: [], input2: [] }, +const merge1Service: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { merge1: Merge1Resource }, createGraph(inputCollections: Input_NN_NN) { @@ -484,8 +484,8 @@ class MergeReduceResource implements Resource { } } -const mergeReduceService: SkipService = { - initialData: { input1: [], input2: [] }, +const mergeReduceService: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { mergeMapReduce: MergeMapReduceResource, mergeReduce: MergeReduceResource, @@ -516,8 +516,8 @@ class JsonParamsResource implements Resource { return cs.input.map(OffsetMapper, this.offset); } } -const jsonParamsService: SkipService = { - initialData: { input: [] }, +const jsonParamsService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { jsonParams: JsonParamsResource }, createGraph(inputs: Input_NN) { return inputs; @@ -551,8 +551,8 @@ class JSONExtractResource implements Resource { } } -const jsonExtractService: SkipService = { - initialData: { input: [] }, +const jsonExtractService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { jsonExtract: JSONExtractResource }, createGraph(inputCollections: Input_NJP) { @@ -570,8 +570,8 @@ class BooleanKeyResource implements Resource { } } -const booleanRoundtripService: SkipService = { - initialData: { input: [] }, +const booleanRoundtripService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { booleanKey: BooleanKeyResource }, createGraph(inputCollections: Input_BS) { @@ -659,9 +659,9 @@ class MockExternalResource implements Resource { } // As the MockExternal as state: force to renew on each test -function testExternalService(): SkipService { +function testExternalService(): AnySkipService { return { - initialData: { input1: [], input2: [] }, + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { external: MockExternalResource }, externalServices: { external: new MockExternal() }, @@ -683,8 +683,8 @@ class CResource implements Resource { } } -const initServiceWithExternalService: SkipService = { - initialData: { input: [] }, +const initServiceWithExternalService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { display: CResource }, externalServices: { external: new MockExternal() }, @@ -718,8 +718,8 @@ class Resource2 implements Resource { } } -const multipleResourcesService: SkipService = { - initialData: { input1: [], input2: [] }, +const multipleResourcesService: AnySkipService = { + inputs: { input1: new InputDefinition(), input2: new InputDefinition() }, resources: { resource1: Resource1, resource2: Resource2 }, createGraph(inputCollections: Input_SN_SN) { @@ -896,17 +896,17 @@ const kafka_config = { retry: { multiplier: 1.5 }, logLevel: kafkaLogLevel.NOTHING, }; -function kafkaService(): SkipService { +function kafkaService(): AnySkipService { const kafka = new KafkaExternalService(kafka_config); return { - initialData: { - input: [ + inputs: { + input: new InputDefinition([ [1, [10]], [2, [20]], [3, [30]], [4, [40]], [5, [50]], - ], + ]), }, resources: { resource: KafkaResource }, externalServices: { kafka }, @@ -921,7 +921,7 @@ function kafkaService(): SkipService { // construct a service object like the other tests in this file. const postgresService: ( inresource: boolean, -) => Promise> = async (inresource) => { +) => Promise = async (inresource) => { const postgres = new PostgresExternalService(pg_config); await withAlternateConsoleError( () => {}, @@ -936,12 +936,12 @@ const postgresService: ( ); return { - initialData: { - input: [ + inputs: { + input: new InputDefinition([ [1, [10]], [2, [20]], [3, [30]], - ], + ]), }, resources: { resource: inresource ? PostgresResource : InputResource, @@ -998,9 +998,9 @@ class LazyWithUseExternalServiceResource implements Resource { } } -function lazyWithUseExternalServiceService(): SkipService { +function lazyWithUseExternalServiceService(): AnySkipService { return { - initialData: { input: [] }, + inputs: { input: new InputDefinition() }, resources: { lazy: LazyWithUseExternalServiceResource }, externalServices: { external: new MockExternal() }, @@ -1024,8 +1024,8 @@ class MapWithExceptionResource implements Resource { } } -const mapWithExceptionService: SkipService = { - initialData: { input: [] }, +const mapWithExceptionService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { mapWithException: MapWithExceptionResource }, createGraph(inputCollections: Input_SN) { @@ -1058,9 +1058,9 @@ class MapWithExceptionOnExternalResource implements Resource { } } -function mapWithExceptionOnExternalService(): SkipService { +function mapWithExceptionOnExternalService(): AnySkipService { return { - initialData: { input: [] }, + inputs: { input: new InputDefinition() }, resources: { mapWithException: MapWithExceptionOnExternalResource }, externalServices: { external: new MockExternal() }, @@ -1078,16 +1078,13 @@ class NNResource implements Resource { } } -function initServiceWithExternalServiceFailure(): SkipService< - NamedCollections, - Input_NN -> { +function initServiceWithExternalServiceFailure(): AnySkipService { return { - initialData: {}, + inputs: {}, resources: { display: NNResource }, externalServices: { external: new MockExternal() }, - createGraph(_is: NamedCollections, context: Context) { + createGraph(_is: NamedEagerCollections, context: Context) { const external = context .useExternalResource({ service: "external", @@ -1102,16 +1099,13 @@ function initServiceWithExternalServiceFailure(): SkipService< }; } -function initServiceWithFaillingExternalService(): SkipService< - NamedCollections, - Input_NN -> { +function initServiceWithFaillingExternalService(): AnySkipService { return { - initialData: {}, + inputs: {}, resources: { display: NNResource }, externalServices: { external: new MockExternal() }, - createGraph(_is: NamedCollections, context: Context) { + createGraph(_is: NamedEagerCollections, context: Context) { const external = context .useExternalResource({ service: "external", @@ -1128,8 +1122,8 @@ function initServiceWithFaillingExternalService(): SkipService< // testResourceNotifications -const resourceNotificationsService: SkipService = { - initialData: { input: [] }, +const resourceNotificationsService: AnySkipService = { + inputs: { input: new InputDefinition() }, resources: { resource: NNResource }, createGraph(is: Input_NN) { @@ -1163,11 +1157,11 @@ class ValuesResource implements Resource { } } -const resourceRecomputeNotificationsService: SkipService< - Input_NN_NN, - Input_NN_NN -> = { - initialData: { input1: [[1, [1, 2]]], input2: [[1, [1]]] }, +const resourceRecomputeNotificationsService: AnySkipService = { + inputs: { + input1: new InputDefinition([[1, [1, 2]]]), + input2: new InputDefinition([[1, [1]]]), + }, resources: { resource: ValuesResource }, createGraph(is: Input_NN_NN) { @@ -1242,7 +1236,7 @@ class WithChanges implements ChangeManager { export function initTests( category: string, - initService: (service: SkipService) => Promise, + initService: (service: AnySkipService) => Promise, ) { const it = (title: string, fn?: AsyncFunc) => mit(`${title}[${category}]`, fn); diff --git a/skipruntime-ts/wasm/src/browser.ts b/skipruntime-ts/wasm/src/browser.ts index 0ec904330..f00fe8f11 100644 --- a/skipruntime-ts/wasm/src/browser.ts +++ b/skipruntime-ts/wasm/src/browser.ts @@ -1,10 +1,10 @@ -import type { ServiceInstance, SkipService } from "@skipruntime/core"; +import type { ServiceInstance, AnySkipService } from "@skipruntime/core"; import { initServiceFor } from "./skipruntime_init.js"; import { environment as createEnvironment } from "../skipwasm-std/sk_browser.js"; export async function initService( - service: SkipService, + service: AnySkipService, ): Promise { return await initServiceFor(createEnvironment, service); } diff --git a/skipruntime-ts/wasm/src/internals/skipruntime_module.ts b/skipruntime-ts/wasm/src/internals/skipruntime_module.ts index c0711eb0b..82273d163 100644 --- a/skipruntime-ts/wasm/src/internals/skipruntime_module.ts +++ b/skipruntime-ts/wasm/src/internals/skipruntime_module.ts @@ -10,11 +10,12 @@ import type { } from "../../skipwasm-std/index.js"; import type * as Internal from "@skipruntime/core/internal.js"; import type { + AnySkipService, Reducer, - SkipService, Mapper, LazyCompute, ExternalService, + NamedEagerCollections, Resource, Watermark, HandlerInfo, @@ -80,7 +81,9 @@ export interface FromWasm { // Resource - SkipRuntime_createResource(ref: Handle): ptr; + SkipRuntime_createResource( + ref: Handle>, + ): ptr; // Service @@ -281,11 +284,13 @@ interface ToWasm { // Resource SkipRuntime_Resource__instantiate( - resource: Handle, + resource: Handle>, collections: ptr, ): ptr; - SkipRuntime_deleteResource(resource: Handle): void; + SkipRuntime_deleteResource( + resource: Handle>, + ): void; // ServiceDefinition @@ -456,7 +461,7 @@ export class WasmFromBinding implements FromBinding { } SkipRuntime_createResource( - ref: Handle, + ref: Handle>, ): Pointer { return this.fromWasm.SkipRuntime_createResource(ref); } @@ -867,7 +872,7 @@ class LinksImpl implements Links { // Resource instantiateOfResource( - skresource: Handle, + skresource: Handle>, skcollections: ptr, ): ptr { return this.utils.exportString( @@ -878,7 +883,7 @@ class LinksImpl implements Links { ); } - deleteResource(resource: Handle) { + deleteResource(resource: Handle>) { this.tobinding.SkipRuntime_deleteResource(resource); } @@ -1091,10 +1096,12 @@ class LinksImpl implements Links { export class ServiceInstanceFactory implements Shared { constructor( - private readonly init: (service: SkipService) => Promise, + private readonly init: ( + service: AnySkipService, + ) => Promise, ) {} - initService(service: SkipService): Promise { + initService(service: AnySkipService): Promise { return this.init(service); } diff --git a/skipruntime-ts/wasm/src/node.ts b/skipruntime-ts/wasm/src/node.ts index f54bdeed0..9bc307d9c 100644 --- a/skipruntime-ts/wasm/src/node.ts +++ b/skipruntime-ts/wasm/src/node.ts @@ -1,10 +1,10 @@ -import type { ServiceInstance, SkipService } from "@skipruntime/core"; +import type { ServiceInstance, AnySkipService } from "@skipruntime/core"; import { initServiceFor } from "./skipruntime_init.js"; import { environment as createEnvironment } from "../skipwasm-std/sk_node.js"; export async function initService( - service: SkipService, + service: AnySkipService, ): Promise { return await initServiceFor(createEnvironment, service); } diff --git a/skipruntime-ts/wasm/src/skipruntime_init.ts b/skipruntime-ts/wasm/src/skipruntime_init.ts index 30777f334..b45fa8e2a 100644 --- a/skipruntime-ts/wasm/src/skipruntime_init.ts +++ b/skipruntime-ts/wasm/src/skipruntime_init.ts @@ -9,7 +9,7 @@ import { init as runtimeInit } from "../skipwasm-std/sk_runtime.js"; import { init as posixInit } from "../skipwasm-std/sk_posix.js"; import { init as skjsonInit } from "../skipwasm-json/skjson.js"; import { init as skruntimeInit } from "./internals/skipruntime_module.js"; -import type { SkipService, ServiceInstance } from "@skipruntime/core"; +import type { ServiceInstance, AnySkipService } from "@skipruntime/core"; const modules: ModuleInit[] = [ runtimeInit, @@ -37,7 +37,7 @@ async function wasmUrl(): Promise { export async function initServiceFor( createEnvironment: EnvCreator, - service: SkipService, + service: AnySkipService, ): Promise { const data = await run(wasmUrl, modules, [], createEnvironment); const factory = data.environment.shared.get(