Skip to content
Open
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
8 changes: 6 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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 @@ -12,7 +12,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"],
"plugins": ["lint/prefer-let.ts"]
},
Expand All @@ -29,6 +29,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
6 changes: 6 additions & 0 deletions experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./lib/api.ts";
export {
InstructionQueue,
type Instruction,
} from "./lib/reducer.ts";
export { DelimiterContext } from "./lib/delimiter.ts";
142 changes: 142 additions & 0 deletions lib/api-internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// deno-lint-ignore-file ban-types no-explicit-any
import { createContext } from "./context.ts";
import { useScope } from "./scope.ts";
import type {
Api,
Around,
Context,
Middleware,
Operation,
Scope,
} from "./types.ts";
import type { ScopeInternal } from "./scope-internal.ts";

export interface ApiInternal<A> extends Api<A> {
context: Context<{
max: Partial<Around<A>>[];
min: Partial<Around<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 = createHandle(api, scope);
let member = handle[key];
if (typeof member === "function") {
return member(...args);
} else {
return member;
}
},
around: (decorator, options = { at: "max" }) => ({
*[Symbol.iterator]() {
let scope = yield* useScope();
scope.around(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* useScope();
let target = api.invoke(scope, field, args);

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

function createHandle<A extends {}>(api: ApiInternal<A>, scope: Scope): A {
let handle = Object.create(api.core);
let $scope = scope as ScopeInternal;
for (let key of Object.keys(api.core) as Array<keyof A>) {
let dispatch = (args: unknown[], next: (...args: unknown[]) => unknown) => {
let { min, max } = $scope.reduce(api.context, (sum, current) => {
let min = current.min.flatMap((around) =>
around[key] ? [around[key]] : []
);
let max = current.max.flatMap((around) =>
around[key] ? [around[key]] : []
);

sum.min.push(...min);
sum.max.unshift(...max);

return sum;
}, {
min: [] as Around<A>[typeof key][],
max: [] as Around<A>[typeof key][],
});

let stack = combine(
max.concat(min) as Middleware<unknown[], unknown>[],
);
return stack(args, next);
};

if (typeof api.core[key] === "function") {
handle[key] = (...args: unknown[]) =>
dispatch(args, api.core[key] as (...args: unknown[]) => unknown);
} else {
Object.defineProperty(handle, key, {
enumerable: true,
get() {
return dispatch([], () => api.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))
);
}
55 changes: 55 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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";
import type { Instruction } from "./reducer.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 ReducerApi {
reduce(instruction: Instruction): void;
}

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

export const api: Apis = {
Scope: createApi<ScopeApi>("Scope", {
create() {
throw new TypeError(`no handler for Scope.create()`);
},
*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()"`);
},
}),
Reducer: createApi<ReducerApi>("Reducer", {
reduce() {
throw new TypeError(`no handler for Reducer.reduce()`);
},
}),
};
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";
2 changes: 1 addition & 1 deletion lib/callcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function* callcc<T>(
reject: (error: Error) => Operation<void>,
) => Operation<void>,
): Operation<T> {
let result = withResolvers<Result<T>>();
let result = withResolvers<Result<T>>("await callcc");

let resolve = lift((value: T) => result.resolve(Ok(value)));

Expand Down
22 changes: 11 additions & 11 deletions lib/coroutine.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { api as effection } from "./api.ts";
import { Priority } from "./contexts.ts";
import { DelimiterContext } from "./delimiter.ts";
import { ReducerContext } from "./reducer.ts";
import { Ok } from "./result.ts";
import type { Coroutine, Operation, Scope } from "./types.ts";

const reducerApi = effection.Reducer;

export interface CoroutineOptions<T> {
scope: Scope;
operation(): Operation<T>;
Expand All @@ -12,8 +14,6 @@ export interface CoroutineOptions<T> {
export function createCoroutine<T>(
{ operation, scope }: CoroutineOptions<T>,
): Coroutine<T> {
let reducer = scope.expect(ReducerContext);

let iterator: Coroutine<T>["data"]["iterator"] | undefined = undefined;

let routine = {
Expand All @@ -31,25 +31,25 @@ export function createCoroutine<T>(
next(result) {
routine.data.exit((exitResult) => {
routine.data.exit = (didExit) => didExit(Ok());
reducer.reduce([
scope.expect(Priority),
reducerApi.invoke(routine.scope, "reduce", [[
routine.scope.expect(Priority),
routine,
exitResult.ok ? result : exitResult,
scope.expect(DelimiterContext).validator,
routine.scope.expect(DelimiterContext).validator,
"next",
]);
]]);
});
},
return(result) {
routine.data.exit((exitResult) => {
routine.data.exit = (didExit) => didExit(Ok());
reducer.reduce([
scope.expect(Priority),
reducerApi.invoke(routine.scope, "reduce", [[
routine.scope.expect(Priority),
routine,
exitResult.ok ? result : exitResult,
scope.expect(DelimiterContext).validator,
routine.scope.expect(DelimiterContext).validator,
"return",
]);
]]);
});
},
} as Coroutine<T>;
Expand Down
2 changes: 1 addition & 1 deletion lib/delimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class Delimiter<T>
implements Operation<Maybe<Result<T>>>, ErrorBoundary {
level = 0;
finalized = false;
future = withResolvers<Maybe<Result<T>>>();
future = withResolvers<Maybe<Result<T>>>("await delimiter");
computed = false;
routine?: Coroutine;
outcome?: Maybe<Result<T>>;
Expand Down
4 changes: 2 additions & 2 deletions lib/each.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function each<T>(stream: Stream<T, unknown>): Operation<Iterable<T>> {
scope.set(EachStack, []);
}

let done = withResolvers<void>();
let cxt = withResolvers<EachLoop<T>>();
let done = withResolvers<void>("await each done");
let cxt = withResolvers<EachLoop<T>>("await each context");

yield* spawn(function* () {
let subscription = yield* stream;
Expand Down
Loading
Loading