Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
path: ./build/npm

- name: Publish NPM
run: npm publish --access=public --tag=latest
run: npm publish --access=public --tag=next
working-directory: ./build/npm

publish-jsr:
Expand Down
9 changes: 6 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"name": "@effection/effection",
"exports": "./mod.ts",
"license": "ISC",
"publish": { "include": ["lib", "mod.ts", "README.md"] },
"publish": { "include": ["lib", "mod.ts", "experimental.ts", "README.md"] },
"lock": false,
"tasks": {
"test": "deno test --allow-run --allow-read --allow-env --allow-ffi",
Expand All @@ -11,7 +10,7 @@
"build:jsr": "deno run -A tasks/build-jsr.ts"
},
"lint": {
"rules": { "exclude": ["prefer-const", "require-yield"] },
"rules": { "exclude": ["prefer-const", "require-yield", "ban-types"] },
"exclude": ["build", "docs", "tasks", "bench"],
"plugins": ["lint/prefer-let.ts"]
},
Expand All @@ -28,6 +27,10 @@
"tinyexec": "npm:tinyexec@1.0.1",
"https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "npm:path-to-regexp@8.2.0"
},
"exports": {
".": "./mod.ts",
"./experimental": "./experimental.ts"
},
"nodeModulesDir": "auto",
"workspace": [
"./www"
Expand Down
1 change: 1 addition & 0 deletions experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./lib/api.ts";
220 changes: 220 additions & 0 deletions lib/api-internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// deno-lint-ignore-file ban-types no-explicit-any
import { createContext } from "./context.ts";
import type {
Api,
Around,
Context,
Effect,
Middleware,
Operation,
Scope,
} from "./types.ts";
import { Children } from "./contexts.ts";
import { Ok } from "./result.ts";

export interface ApiInternal<A> extends Api<A> {
context: Context<{
local: Decorator<A>;
total: Decorator<A>;
handle: A;
}>;
core: A;
}

export function createApiInternal<A extends {}>(
name: string,
core: A,
): ApiInternal<A> {
let fields = Object.keys(core) as (keyof A)[];

let context = createContext(`api::${name}`) as ApiInternal<A>["context"];

let api: ApiInternal<A> = {
core,
context,

invoke: (scope, key, args) => {
let handle = scope.get(api.context)?.handle ?? core;

let member = handle[key];
if (typeof member === "function") {
return member(...args);
} else {
return member;
}
},
around: (decorator, options = { at: "max" }) => ({
*[Symbol.iterator]() {
let scope = (yield GetScope) as Scope;
decorateApi(scope, api, decorator, options);
},
}),
operations: fields.reduce((sum, field) => {
if (typeof core[field] === "function") {
return Object.assign(sum, {
[field]: (...args: any) => ({
*[Symbol.iterator]() {
let scope = (yield GetScope) as Scope;
let target = api.invoke(scope, field, args);

return isOperation(target) ? yield* target : target;
},
}),
});
} else {
return Object.assign(sum, {
[field]: {
*[Symbol.iterator]() {
let scope = (yield GetScope) as Scope;
let target = api.invoke(scope, field, [] as any);
return isOperation(target) ? yield* target : target;
},
},
});
}
}, {} as Api<A>["operations"]),
};
return api;
}

export function decorateApi<A>(
scope: Scope,
api: ApiInternal<A>,
decorator: Partial<Around<A>>,
options?: {
at: "min" | "max";
},
) {
// read existing total and local
let current = scope.get(api.context) ?? { total: {}, local: {} };

let local = decorate(current.local, {
[options?.at ?? "max"]: decorator,
});

if (!scope.hasOwn(api.context)) {
scope.set(api.context, {
local,
total: current.total,
handle: api.core,
});
} else {
current.local = local;
}

install(scope, api, current.total);
}

function decorate<A>(base: Decorator<A>, next: Decorator<A>): Decorator<A> {
return {
max: base.max ? next.max ? append(base.max, next.max) : base.max : next.max,
min: next.min ? base.min ? append(next.min, base.min) : next.min : base.min,
};
}

function append<A>(
outer: Partial<Around<A>>,
inner: Partial<Around<A>>,
): Partial<Around<A>> {
let result: Partial<Around<A>> = { ...outer };
for (let key of Object.keys(inner) as (keyof A)[]) {
let current = outer[key];
let decoration = inner[key];
if (!current) {
result[key] = decoration;
} else {
let pair = [current, decoration] as Middleware<unknown[], unknown>[];
result[key] = combine(pair) as Around<A>[keyof A];
}
}
return result;
}

function install<A>(
scope: Scope,
api: ApiInternal<A>,
total: Decorator<A>,
): void {
if (scope.hasOwn(api.context)) {
let context = scope.expect(api.context);

context.total = total;

total = decorate(context.total, context.local);

context.handle = createApiHandle(total, api.core);
}

for (let child of scope.expect(Children)) {
install(child, api, total);
}
}

function createApiHandle<A>(decoration: Decorator<A>, core: A): A {
let around = decoration.max
? decoration.min ? append(decoration.max, decoration.min) : decoration.max
: decoration.min;
if (!around) {
return core;
}
let handle: A = {} as A;
for (let key of Object.keys(core as object) as Array<keyof A>) {
let middleware = around[key] as Middleware<unknown[], unknown> | undefined;
if (middleware) {
if (typeof core[key] === "function") {
handle[key] = ((...args: unknown[]) =>
middleware(args, core[key] as (...args: unknown[]) => unknown)) as A[
keyof A
];
} else {
Object.defineProperty(handle, key, {
enumerable: true,
get() {
return middleware([], () => core[key]);
},
});
}
} else {
handle[key] = core[key];
}
}
return handle;
}

function isOperation<T>(
target: Operation<T> | T,
): target is Operation<T> {
return target && !isNativeIterable(target) &&
typeof (target as Operation<T>)[Symbol.iterator] === "function";
}

function isNativeIterable(target: unknown): boolean {
return (
typeof target === "string" || Array.isArray(target) ||
target instanceof Map || target instanceof Set
);
}

function combine<TArgs extends unknown[], TReturn>(
middlewares: Middleware<TArgs, TReturn>[],
): Middleware<TArgs, TReturn> {
if (middlewares.length === 0) {
return (args, next) => next(...args);
}
return middlewares.reduceRight((sum, middleware) => (args, next) =>
middleware(args, (...args) => sum(args, next))
);
}

interface Decorator<A> {
min?: Partial<Around<A>>;
max?: Partial<Around<A>>;
}

const GetScope: Effect<Scope> = {
description: "Fast, non-typesafe lookup of co-routine scope",
enter: (resolve, routine) => {
resolve(Ok(routine.scope));
return (didExit) => didExit(Ok());
},
};
46 changes: 46 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// deno-lint-ignore-file ban-types
import type { Api, Context, Operation, Scope } from "./types.ts";
import { createApiInternal } from "./api-internal.ts";
import type { ScopeInternal } from "./scope-internal.ts";

export function createApi<A extends {}>(name: string, core: A): Api<A> {
return createApiInternal(name, core);
}

interface ScopeApi {
create(parent: Scope): [Scope, () => Operation<void>];
destroy(scope: Scope): Operation<void>;
set<T>(scope: Scope, context: Context<T>, value: T): T;
delete<T>(scope: Scope, context: Context<T>): boolean;
}

interface MainApi {
main(body: (args: string[]) => Operation<void>): Promise<void>;
}

interface Apis {
Scope: Api<ScopeApi>;
Main: Api<MainApi>;
}

export const api: Apis = {
Scope: createApi<ScopeApi>("Scope", {
create() {
throw new TypeError(`no handler for Scope.create()`);
},
destroy(scope) {
return (scope as ScopeInternal).destroy();
},
set(scope, context, value) {
return (scope as ScopeInternal).contexts[context.name] = value;
},
delete(scope, context) {
return delete (scope as ScopeInternal).contexts[context.name];
},
}),
Main: createApi<MainApi>("Main", {
main() {
throw new TypeError(`missing handler for "main()"`);
},
}),
};
67 changes: 67 additions & 0 deletions lib/attributes-internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createContext } from "./context.ts";
import { useScope } from "./scope.ts";
import type { Operation, Scope } from "./types.ts";

/**
* Serializable name/value pairs that can be used for visualizing and
* inpsecting Effection scopes. There will always be at least a name
* in the attributes.
*/
export type Attributes =
& { name: string }
& Record<string, string | number | boolean>;

const AttributesContext = createContext<Attributes>(
"@effection/attributes",
{ name: "anonymous" },
);

/**
* Add metadata to the current {@link Scope} that can be used for
* display and debugging purposes.
*
* Calling `useAttributes()` multiple times will add new attributes
* and overwrite attributes of the same name, but it will not erase
* old ones.
*
* @example
* ```ts
* function useServer(port: number): Operation<Server> {
* return resource(function*(provide) {
* yield* useAttributes({ name: "Server", port });
* let server = createServer();
* server.listen();
* try {
* yield* provide(server);
* } finally {
* server.close();
* }
* });
* }
* ```
*
* @param attrs - attributes to add to this {@link Scope}
* @returns an Oeration adding `attrs` to the current scope
* @since 4.1
*/
export function* useAttributes(attrs: Partial<Attributes>): Operation<void> {
let scope = yield* useScope();

let current = scope.hasOwn(AttributesContext)
? scope.expect(AttributesContext)
: AttributesContext.defaultValue!;

scope.set(AttributesContext, { ...current, ...attrs });
}

/**
* Get the unique attributes of this {@link Scope}. Attributes are not
* inherited and only the attributes explicitly assigned to this scope
* will be returned.
*/
export function getAttributes(scope: Scope) {
if (scope.hasOwn(AttributesContext)) {
return scope.expect(AttributesContext);
}
return AttributesContext.defaultValue as Attributes;
}
1 change: 1 addition & 0 deletions lib/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { type Attributes, useAttributes } from "./attributes-internal.ts";
Loading
Loading