commit 84569b8c90e719870a1ade8a3d8834b8a46d07cd Author: Tony Garnock-Jones Date: Wed Feb 17 20:57:15 2021 +0100 Initial work on novy-syndicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..946dac4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.d.ts +*.js +*.js.map +package-lock.json +node_modules/ diff --git a/actor.ts b/actor.ts new file mode 100644 index 0000000..304d709 --- /dev/null +++ b/actor.ts @@ -0,0 +1,115 @@ +import { Value } from 'preserves'; + +type Assertion = Value; + +type ActorId = number; + +type ExitReason = null | { ok: true } | { ok: false, err: Error }; + +let nextActorId: ActorId = 0; + +type AssertionHandle = object; + +const assert = Symbol('assert'); +const retract = Symbol('retract'); +const message = Symbol('message'); + +type RestParameters any> = + T extends (arg: any, ...args: infer P) => any ? P : never; + +type Event = + | { type: typeof assert, args: RestParameters } + | { type: typeof retract, args: RestParameters } + | { type: typeof message, args: RestParameters } + +type Action = Event; + +class Peer { + readonly actor: Actor; + readonly target: Facet; + + constructor(actor: Actor, target: Facet) { + this.actor = actor; + this.target = target; + } +} + +interface Facet { + [assert](turn: Turn, assertion: Assertion, handle: AssertionHandle): void; + [retract](turn: Turn, handle: AssertionHandle): void; + [message](turn: Turn, message: Assertion): void; +} + +class Actor { + readonly id: ActorId = nextActorId++; + exitReason: ExitReason = null; + + scheduleTurn(target: Facet, turn: Turn) { + queueMicrotask(() => { + if (this.alive) { + try { + const event = turn.event; + (target as any)[event.type](turn, ...event.args); + // ^ This is safe. Try replacing it with the following to see: + // + // switch (event.type) { + // case assert: target[event.type](turn, ...event.args); break; + // case retract: target[event.type](turn, ...event.args); break; + // case message: target[event.type](turn, ...event.args); break; + // } + turn.finish(); + } catch (err) { + this.terminateWith({ ok: false, err }); + } + } + }); + } + + get alive(): boolean { + return this.exitReason === null; + } + + stop() { + this.terminateWith({ ok: true }); + } + + terminateWith(reason: Exclude) { + if (this.alive) { + this.exitReason = reason; + // TODO cleanup + } + } +} + +class Turn { + readonly recipient: Actor; + readonly event: Event; + readonly actions: Map> = new Map(); + + constructor(recipient: Actor, event: Event) { + this.recipient = recipient; + this.event = event; + } + + enqueueAction(peer: Peer, action: Action) { + let targetMap = this.actions.get(peer.actor); + if (targetMap === void 0) { + targetMap = new Map(); + this.actions.set(peer.actor, targetMap); + } + let actions = targetMap.get(peer.target); + if (actions === void 0) { + actions = []; + targetMap.set(peer.target, actions); + } + actions.push(action); + } + + finish() { + this.actions.forEach((targetMap, actor) => { + targetMap.forEach((actions, target) => { + actions.forEach(action => actor.scheduleTurn(target, new Turn(actor, action))); + }); + }); + } +} diff --git a/i0.ts b/i0.ts new file mode 100644 index 0000000..52a32d1 --- /dev/null +++ b/i0.ts @@ -0,0 +1,143 @@ +type ValidSelector = string | number | symbol; + +export type EventMessage = { + selector: Selector, + args: Args, +}; + +export type RequestMessage = { + selector: Selector, + args: Args, + callback: (result: Result) => void, +}; + +export type Message = + void extends Result ? EventMessage : RequestMessage; + +// export type EventMessage = { +// selector: Selector, +// args: Args, +// }; +// +// export type RequestMessage = { +// selector: Selector, +// args: Args, +// callback: (result: Result) => void, +// }; +// +// export type Message = +// | EventMessage +// | RequestMessage; +// +// type Messages1 = { +// [K in keyof I]: (I[K] extends (...args: [...ContextArgs, ...infer P]) => infer Q +// ? (void extends Q ? EventMessage : RequestMessage) +// : never); +// }; + +type Messages1 = { + [K in keyof I]: (I[K] extends (...args: [...ContextArgs, ...infer P]) => infer Q + ? Message + : never); +}; + +// type Proj1 = I[K]; +// type Proj = Proj1; +// export type Messages = Proj>; + +export type Messages = Messages1[keyof I]; + +export type Methods = { + [S in M['selector']]: ( + M extends RequestMessage ? (...args: [...ContextArgs, ...P]) => R : + M extends EventMessage ? (... args: [...ContextArgs, ...P]) => void : + never); +}; + +// interface I { +// m1(a: string, b: number): boolean; +// m2(): void; +// m3(n: number): void; +// m4(x: [string, string]): { k: string, j: string }; +// m5(a: string, b: string[]): number; +// v: string; +// w: number; +// }; +// +// const a = { a(): string { console.log('in a'); return 'hi'; }, b(): void { console.log('in b'); } }; +// type A = typeof a; +// type A1 = Messages; +// type A2 = Methods; +// const b: A2 = a; + +// export function performRequest( +// i: { [s in S]: (...args: [...ContextArgs, ...A]) => R }, +// m: { selector: S, args: A, callback: (result: R) => void }, +// ...ctxt: ContextArgs) +// : R +// { +// const r = i[m.selector](...ctxt, ... m.args); +// m.callback(r); +// return r; +// } + +// export function performEvent( +// i: { [s in S]: (...args: [...ContextArgs, ...A]) => void }, +// m: { selector: S, args: A }, +// ...ctxt: ContextArgs) +// : void +// { +// i[m.selector](...ctxt, ...m.args); +// } + +// function send( +// i: { [s in S]: (...args: A) => R }, +// m: { selector: S, args: A, callback?: (result: R) => void }) +// : R; +// function send( +// i: { [s in S]: (...args: A) => void }, +// m: { selector: S, args: A }) +// : void; +// function send( +// i: { [s in S]: (...args: A) => R }, +// m: { selector: S, args: A, callback?: (result: R) => void }) +// : R +// // function send>(i: I, m: M): [M, I] extends [RequestMessage, Methods] ? R : void +// { +// const r = i[m.selector](... m.args); +// m.callback?.(r); +// return r; +// } + +export function perform, S extends ValidSelector, M extends RequestMessage, ContextArgs extends any[] = []>(i: I, m: M, ...ctxt: ContextArgs) +: [M, I] extends [RequestMessage, Methods] ? R : void; +export function perform, S extends ValidSelector, M extends EventMessage, ContextArgs extends any[] = []>(i: I, m: M, ...ctxt: ContextArgs): void; +export function perform, S extends ValidSelector, M extends RequestMessage, R, ContextArgs extends any[] = []>(i: I, m: M, ...ctxt: ContextArgs): R +{ + const r = i[m.selector](...ctxt, ... m.args); + m.callback?.(r); + return r; +} + +// function perform, S extends ValidSelector, M extends RequestMessage>(i: I, m: M) +// : [M, I] extends [RequestMessage, Methods] ? R : void; +// function perform, S extends ValidSelector, M extends EventMessage>(i: I, m: M): void; +// function perform, S extends ValidSelector, M extends RequestMessage>(i: I, m: M): R +// { +// const r = i[m.selector](... m.args); +// m.callback?.(r); +// return r; +// } + +// const aa = perform(a, { selector: 'a', args: [], callback: (_r: string) => {} }); +// const bb = perform(a, { selector: 'b', args: [], callback: (_r: void) => { console.log('bb'); } }); +// const bb2 = perform(a, { selector: 'b', args: [] }); +// // perform({ a(): string { return 'hi' } }, { selector: 'a', args: [123], callback: (_r: string) => {} }); + +// type Q = Messages; +// type M = Methods; +// type N = M; +// type R = Messages>; +// type S = R; + +// const x: Q = { selector: 'm2', args: [] }; diff --git a/i2.ts b/i2.ts new file mode 100644 index 0000000..faae355 --- /dev/null +++ b/i2.ts @@ -0,0 +1,87 @@ +type ValidSelector = string | number | symbol + +export type Message = + Args extends never[] + ? { selector: Selector, args: [], callback: (result: Result) => void } + : { selector: Selector, args: Args, callback: (result: Result) => void } + +type MessagesProduct = { + [K in keyof I]: (I[K] extends (...args: infer P) => infer Q ? Message : never); +} + +export type Messages = MessagesProduct[keyof I] + +export type Methods = { + [S in M['selector']]: ( + M extends Message ? (...args: P) => R : + never); +} + +export function perform, + S extends ValidSelector, + M extends Message>( + i: I, + m: M): void +{ + m.callback(i[m.selector](... m.args)); +} + +//--------------------------------------------------------------------------- + +interface I { + m1(a: string, b: number): boolean; + m2(): void; + m3(n: number): void; + m4(x: [string, string]): { k: string, j: string }; + m5(a: string, b: string[]): number; + v: string; + w: number; +} + +type M = Messages +// type M = +// | Message<"m1", [a: string, b: number], boolean> +// | Message<"m2", [], void> +// | Message<"m3", [n: number], void> +// | Message<"m4", [x: [string, string]], { k: string; j: string }> +// | Message<"m5", [a: string, b: string[]], number> +// type M = +// | { selector: "m1", args: [a: string, b: number], callback: (result: boolean) => void } +// | { selector: "m2", args: [], callback: (result: void) => void } +// | { selector: "m3", args: [n: number], callback: (result: void) => void } +// | { selector: "m4", args: [x: [string, string]], callback: (result: { k: string; j: string }) => void } +// | { selector: "m5", args: [a: string, b: string[]], callback: (result: number) => void } +// type M = +// | { selector: "m1", args: [string, number], callback: (result: boolean) => void } +// | { selector: "m2", args: [], callback: (result: void) => void } +// | { selector: "m3", args: [number], callback: (result: void) => void } +// | { selector: "m4", args: [[string, string]], callback: (result: { k: string; j: string }) => void } +// | { selector: "m5", args: [string, string[]], callback: (result: number) => void } + +type I2 = Methods +// type I2 = { +// m1: (a: string, b: number) => boolean; +// m2: () => void; +// m3: (n: number) => void; +// m4: (x: [string, string]) => { k: string; j: string }; +// m5: (a: string, b: string[]) => number; +// }; + +type X = I2 + +const a = { + a(): string { console.log('in a'); return 'hi'; }, + b(): void { console.log('in b'); }, + c(x: number): number { console.log('in c:', x); return x * 2; }, + v(x: number, y: string, z: boolean): string { return `x ${x} y ${y} z ${z}`; }, +}; +type A = typeof a; +type A1 = Messages; +type A2 = Methods; +const b: A2 = a; + +const aa = perform(a, { selector: 'a', args: [], callback: (_r: string) => {} }); +const bb = perform(a, { selector: 'b', args: [], callback: (_r: void) => { console.log('bb'); } }); +const cc = perform(a, { selector: 'c', args: [123], callback: (r: number) => { console.log('cc', r); } }); +const vv = perform(a, { selector: 'v', args: [123, 'hi', true], callback: (r: string) => { console.log('vv', r); } }); +perform({ a(): string { return 'hi' } }, { selector: 'a', args: [123], callback: (_r: string) => { console.log('x'); } }); diff --git a/interfaces.ts b/interfaces.ts new file mode 100644 index 0000000..1f31a88 --- /dev/null +++ b/interfaces.ts @@ -0,0 +1,67 @@ +// This Tuple type (and tuple() function) is a hack to induce +// TypeScript to infer tuple types rather than array types. (Source: +// https://github.com/microsoft/TypeScript/issues/27179#issuecomment-422606990) +// +// Without it, [123, 'hi', true] will often get the type (string | +// number | boolean)[] instead of [number, string, boolean]. +// +export type Tuple = any[] | [any]; +export const tuple = (... args: A) => args; + +// Type ValidSelector captures TypeScript's notion of a valid object +// property name. +// +export type ValidSelector = string | number | symbol; + +export type EventMessage = + { selector: Selector, args: Args }; + +export type RequestMessage> = + { selector: Selector, args: Args, callback: (result: Result) => void }; + +export type Message = + void extends Result ? EventMessage : RequestMessage; + +// Function message() is needed for similar reasons to tuple() above: +// to help TypeScript infer the correct literal type for the selector +// (as well as the arguments). +// +export const message = (m: Message) => m; + +type MessagesProduct = { + [K in keyof I]: (I[K] extends (...args: [...ContextArgs, ...infer P]) => infer Q + ? Message + : never); +}; + +export type Messages = MessagesProduct[keyof I]; + +export type Methods = { + [S in M['selector']]: ( + M extends RequestMessage + ? (void extends R ? never : (...args: [...ContextArgs, ...P]) => R) + : (M extends EventMessage + ? (...args: [...ContextArgs, ...P]) => void + : never)); +}; + +export function perform, + S extends ValidSelector, + M extends RequestMessage, + ContextArgs extends any[] = []> + (i: I, m: M, ...ctxt: ContextArgs): (M extends RequestMessage ? R : never); +export function perform, + S extends ValidSelector, + M extends EventMessage, + ContextArgs extends any[] = []> + (i: I, m: M, ...ctxt: ContextArgs): void; +export function perform, + S extends ValidSelector, + M extends RequestMessage, + ContextArgs extends any[] = []> + (i: I, m: M, ...ctxt: ContextArgs): any +{ + const r = i[m.selector](...ctxt, ... m.args); + m.callback?.(r); + return r; +} diff --git a/interfaces_test.ts b/interfaces_test.ts new file mode 100644 index 0000000..1ab853e --- /dev/null +++ b/interfaces_test.ts @@ -0,0 +1,40 @@ +import { tuple, message, Methods, Messages, perform } from './interfaces.js'; + +const m = message({ + selector: 'b', + args: [123, 'hi', true], + callback: (result: number) => console.log('result:', result), +}); +const m2 = message({ + selector: 'c', + args: [], + // callback: (result: void) => console.log('result:', result), +}); +type X = Methods; +type Y = Messages; + +perform({ + a(ctxt: string, x: number): string { return `${ctxt}(${x + 1})`; }, + b(ctxt: string, y: number): number { return y * 2; }, + z(ctxt: string, ...v: number[]) { return 3; }, + v(ctxt: string, x: number, y: string, z: boolean): string { + console.log('in v'); + return `ctxt ${ctxt} x ${x} y ${y} z ${z}`; + }, + w(ctxt: string, m: [string, number]) { console.log('w', ctxt, m); }, + w2(ctxt: string, x: number, ...m: [string, number][]) { console.log('w2', ctxt, x, m); }, +}, + +// { +// selector: 'w2', +// args: [99, tuple('hi', 123), tuple('hi', 123)], +// callback: (result: void) => console.log('result:', result) +// }, + +{ + selector: 'v', + args: [123, 'hi', true], + callback: (result: string) => console.log('result:', result) +}, + +'C'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d9064a9 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "typescript": "^4.1.5" + }, + "dependencies": { + "preserves": "^0.5.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cfb0543 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["es2017", "dom"], + "declaration": true, + "baseUrl": ".", + "rootDir": ".", + "outDir": ".", + "declarationDir": ".", + "esModuleInterop": true, + "moduleResolution": "node", + "module": "commonjs", + "sourceMap": true, + "strict": true + }, + "include": ["**/*.ts"], + "exclude": [] +}