@xandreed/runtime 0.3.0 → 0.8.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/runtime
2
2
 
3
- Efferent composable agent framework, 0.3.0. Bun on Linux.
3
+ Efferent composable agent framework, 0.8.0-next.0. Bun on Linux.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
package/dist/config.d.ts CHANGED
@@ -17,67 +17,67 @@ export declare const loadConfig: (options: {
17
17
  }) => Effect.Effect<{
18
18
  config: {
19
19
  profile: string;
20
+ version: 1;
21
+ system?: string | undefined;
20
22
  plugins?: readonly {
21
23
  readonly id: string;
24
+ readonly use: string;
22
25
  readonly options?: {
23
26
  readonly [x: string]: unknown;
24
27
  } | undefined;
25
- readonly use: string;
26
28
  readonly enabled?: boolean | undefined;
27
29
  }[] | undefined;
28
30
  bindings?: {
29
31
  readonly [x: string]: string;
30
32
  } | undefined;
31
- system?: string | undefined;
32
- version: 1;
33
33
  profiles?: {
34
34
  readonly [x: string]: {
35
+ readonly system?: string | undefined;
35
36
  readonly plugins?: readonly {
36
37
  readonly id: string;
38
+ readonly use: string;
37
39
  readonly options?: {
38
40
  readonly [x: string]: unknown;
39
41
  } | undefined;
40
- readonly use: string;
41
42
  readonly enabled?: boolean | undefined;
42
43
  }[] | undefined;
43
44
  readonly bindings?: {
44
45
  readonly [x: string]: string;
45
46
  } | undefined;
46
- readonly system?: string | undefined;
47
47
  };
48
48
  } | undefined;
49
49
  };
50
50
  sources: {
51
51
  path: string;
52
52
  config: {
53
+ readonly version: 1;
54
+ readonly system?: string | undefined;
53
55
  readonly plugins?: readonly {
54
56
  readonly id: string;
57
+ readonly use: string;
55
58
  readonly options?: {
56
59
  readonly [x: string]: unknown;
57
60
  } | undefined;
58
- readonly use: string;
59
61
  readonly enabled?: boolean | undefined;
60
62
  }[] | undefined;
61
63
  readonly bindings?: {
62
64
  readonly [x: string]: string;
63
65
  } | undefined;
64
- readonly system?: string | undefined;
65
- readonly version: 1;
66
66
  readonly profile?: string | undefined;
67
67
  readonly profiles?: {
68
68
  readonly [x: string]: {
69
+ readonly system?: string | undefined;
69
70
  readonly plugins?: readonly {
70
71
  readonly id: string;
72
+ readonly use: string;
71
73
  readonly options?: {
72
74
  readonly [x: string]: unknown;
73
75
  } | undefined;
74
- readonly use: string;
75
76
  readonly enabled?: boolean | undefined;
76
77
  }[] | undefined;
77
78
  readonly bindings?: {
78
79
  readonly [x: string]: string;
79
80
  } | undefined;
80
- readonly system?: string | undefined;
81
81
  };
82
82
  } | undefined;
83
83
  };
package/dist/config.js CHANGED
@@ -17,11 +17,11 @@ export const mergeConfig = (base, overlay) => ({
17
17
  plugins: mergeEntries(base.plugins ?? [], overlay.plugins ?? []),
18
18
  bindings: { ...base.bindings, ...overlay.bindings },
19
19
  profiles: { ...base.profiles, ...overlay.profiles }
20
- }), decodeConfig = (input, source) => Schema.decodeUnknown(HarnessConfig)(input, { onExcessProperty: "error" }).pipe(Effect.mapError((error) => new HarnessError({ code: "config.invalid", message: `${source}: ${String(error)}` })));
20
+ }), decodeConfig = (input, source) => Schema.decodeUnknownEffect(HarnessConfig)(input, { onExcessProperty: "error", reportInput: !0 }).pipe(Effect.mapError((error) => new HarnessError({ code: "config.invalid", message: `${source}: ${String(error)}` })));
21
21
  const io = (action, label) => Effect.tryPromise({
22
22
  try: action,
23
23
  catch: (error) => new HarnessError({ code: "config.io", message: `${label}: ${String(error)}` })
24
- }), exists = (path) => Effect.tryPromise({ try: () => access(path), catch: () => !1 }).pipe(Effect.as(!0), Effect.orElseSucceed(() => !1)), readJson = (path) => io(() => readFile(path, "utf8"), path).pipe(Effect.flatMap((text) => Schema.decodeUnknown(Schema.parseJson(HarnessConfig))(text, { onExcessProperty: "error" })), Effect.mapError((error) => new HarnessError({ code: "config.invalid", message: `${path}: ${String(error)}` }))), readBase = (directory) => Effect.gen(function* () {
24
+ }), exists = (path) => Effect.tryPromise({ try: () => access(path), catch: () => !1 }).pipe(Effect.as(!0), Effect.orElseSucceed(() => !1)), readJson = (path) => io(() => readFile(path, "utf8"), path).pipe(Effect.flatMap((text) => Schema.decodeUnknownEffect(Schema.fromJsonString(HarnessConfig))(text, { onExcessProperty: "error", reportInput: !0 })), Effect.mapError((error) => new HarnessError({ code: "config.invalid", message: `${path}: ${String(error)}` }))), readBase = (directory) => Effect.gen(function* () {
25
25
  const json = join(directory, "efferent.config.json"), ts = join(directory, "efferent.config.ts"), found = yield* Effect.filter([json, ts], exists);
26
26
  if (found.length > 1)
27
27
  return yield* Effect.fail(new HarnessError({ code: "config.ambiguous", message: `Choose one base configuration in ${directory}: JSON or TypeScript` }));
package/dist/graph.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Context, Effect, Schema, Scope } from "effect";
2
2
  import { createHash } from "node:crypto";
3
- import { HarnessError, PLUGIN_API_VERSION } from "@xandreed/core";
4
- const invalid = (message) => Effect.fail(new HarnessError({ code: "config.graph", message }));
3
+ import { HarnessError, PLUGIN_API_VERSION, SUPPORTED_PLUGIN_API_VERSIONS } from "@xandreed/core";
4
+ const invalid = (message) => Effect.fail(new HarnessError({ code: "config.graph", message })), contributes = (node) => node.plugin.contributes ?? [], optional = (node) => node.plugin.optional ?? [];
5
5
  export const resolveGraph = (config, plugins, external = []) => Effect.gen(function* () {
6
6
  const duplicatePlugin = plugins.find((plugin, index) => plugins.findIndex((other) => other.id === plugin.id) !== index);
7
7
  if (duplicatePlugin !== void 0)
@@ -13,10 +13,10 @@ export const resolveGraph = (config, plugins, external = []) => Effect.gen(funct
13
13
  const plugin = plugins.find((candidate) => candidate.id === entry.use);
14
14
  if (plugin === void 0)
15
15
  return yield* invalid(`plugin ${entry.use} is not installed (instance ${entry.id})`);
16
- if (plugin.apiVersion !== PLUGIN_API_VERSION)
16
+ if (!SUPPORTED_PLUGIN_API_VERSIONS.includes(plugin.apiVersion))
17
17
  return yield* invalid(`${entry.id}: plugin API ${plugin.apiVersion} is incompatible with ${PLUGIN_API_VERSION}`);
18
18
  const options = { ...plugin.defaults, ...entry.options };
19
- yield* Schema.decodeUnknown(plugin.schema)(options, { onExcessProperty: "error" }).pipe(Effect.mapError((error) => new HarnessError({ code: "config.options", plugin: entry.id, message: String(error) })));
19
+ yield* Schema.decodeUnknownEffect(plugin.schema)(options, { onExcessProperty: "error", reportInput: !0 }).pipe(Effect.mapError((error) => new HarnessError({ code: "config.options", plugin: entry.id, message: String(error) })));
20
20
  return { entry, plugin, options };
21
21
  })), keys = [...new Set(nodes.flatMap((node) => node.plugin.provides))], providers = Object.fromEntries(yield* Effect.forEach(keys, (key) => Effect.gen(function* () {
22
22
  const candidates = nodes.filter((node) => node.plugin.provides.includes(key)), binding = config.bindings?.[key];
@@ -27,29 +27,41 @@ export const resolveGraph = (config, plugins, external = []) => Effect.gen(funct
27
27
  return [key, binding ?? candidates[0].entry.id];
28
28
  })));
29
29
  yield* Effect.forEach(Object.keys(config.bindings ?? {}), (key) => keys.includes(key) ? Effect.void : invalid(`binding references an unavailable service: ${key}`));
30
- yield* Effect.forEach(nodes, (node) => Effect.forEach(node.plugin.requires, (key) => {
31
- const provider = nodes.find((candidate) => candidate.entry.id === providers[key]);
32
- if (provider === void 0 && !external.includes(key))
30
+ const contributors = (key, except) => nodes.filter((node) => node !== except && contributes(node).includes(key)), contributed = new Set(nodes.flatMap(contributes));
31
+ yield* Effect.forEach(nodes, (node) => Effect.forEach([...node.plugin.requires, ...optional(node)], (key) => {
32
+ const provider = nodes.find((candidate) => candidate.entry.id === providers[key]), isCapability = contributed.has(key);
33
+ if (provider !== void 0 && isCapability)
34
+ return invalid(`${key} is both provided and contributed`);
35
+ if (provider === void 0 && !isCapability && !external.includes(key) && node.plugin.requires.includes(key))
33
36
  return invalid(`${node.entry.id} requires missing service ${key}`);
34
- if (node.plugin.scope === "runtime" && provider?.plugin.scope === "session")
37
+ const sources = isCapability ? contributors(key, node) : provider === void 0 ? [] : [provider];
38
+ if (node.plugin.scope === "runtime" && sources.some((source) => source.plugin.scope === "session"))
35
39
  return invalid(`${node.entry.id}: runtime plugins cannot depend on session service ${key}`);
36
40
  return Effect.void;
37
41
  }));
38
42
  const order = (remaining, sorted) => {
39
43
  if (remaining.length === 0)
40
44
  return Effect.succeed(sorted);
41
- const ready = remaining.filter((node) => node.plugin.requires.every((key) => external.includes(key) || sorted.some((candidate) => candidate.entry.id === providers[key])));
45
+ const ready = remaining.filter((node) => [...node.plugin.requires, ...optional(node)].every((key) => external.includes(key) || (contributed.has(key) ? contributors(key, node).every((source) => sorted.includes(source)) : providers[key] === void 0 ? !node.plugin.requires.includes(key) : sorted.some((candidate) => candidate.entry.id === providers[key]))));
42
46
  if (ready.length === 0)
43
47
  return invalid(`dependency cycle: ${remaining.map((node) => node.entry.id).join(" \u2192 ")}`);
44
48
  return Effect.suspend(() => order(remaining.filter((node) => !ready.includes(node)), [...sorted, ...ready]));
45
49
  };
46
50
  return { nodes: yield* order(nodes, []), providers, config };
47
- }), activateGraph = (graph, lifetime, services, scope) => Effect.reduce(graph.nodes.filter((node) => node.plugin.scope === lifetime), services, (context, node) => Effect.gen(function* () {
48
- const dependencies = Context.unsafeMake(new Map(node.plugin.requires.flatMap((key) => context.unsafeMap.has(key) ? [[key, context.unsafeMap.get(key)]] : []))), built = yield* Scope.extend(node.plugin.build(node.options, dependencies), scope), missing = node.plugin.provides.filter((key) => !built.unsafeMap.has(key));
51
+ });
52
+ const contributedKeys = (graph) => new Set(graph.nodes.flatMap(contributes)), asArray = (value) => Array.isArray(value) ? value : [];
53
+ export const activateGraph = (graph, lifetime, services, scope) => Effect.reduce(graph.nodes.filter((node) => node.plugin.scope === lifetime), () => services, (context, node) => Effect.gen(function* () {
54
+ const dependencies = Context.makeUnsafe(new Map([...node.plugin.requires, ...optional(node)].flatMap((key) => context.mapUnsafe.has(key) ? [[key, context.mapUnsafe.get(key)]] : contributedKeys(graph).has(key) ? [[key, []]] : []))), built = yield* Scope.provide(node.plugin.build(node.options, dependencies), scope), missing = [...node.plugin.provides, ...contributes(node)].filter((key) => !built.mapUnsafe.has(key));
49
55
  if (missing.length > 0)
50
56
  return yield* invalid(`${node.entry.id} did not provide declared services: ${missing.join(", ")}`);
51
- const selected = new Map(node.plugin.provides.flatMap((key) => graph.providers[key] === node.entry.id ? [[key, built.unsafeMap.get(key)]] : []));
52
- return Context.merge(context, Context.unsafeMake(selected));
57
+ const invalidCapability = contributes(node).find((key) => !Array.isArray(built.mapUnsafe.get(key)));
58
+ if (invalidCapability !== void 0)
59
+ return yield* invalid(`${node.entry.id} contributed a non-array value to ${invalidCapability}`);
60
+ const selected = new Map([
61
+ ...node.plugin.provides.flatMap((key) => graph.providers[key] === node.entry.id ? [[key, built.mapUnsafe.get(key)]] : []),
62
+ ...contributes(node).map((key) => [key, [...asArray(context.mapUnsafe.get(key)), ...asArray(built.mapUnsafe.get(key))]])
63
+ ]);
64
+ return Context.merge(context, Context.makeUnsafe(selected));
53
65
  })), graphFingerprint = (graph, scope) => createHash("sha256").update(JSON.stringify({
54
66
  nodes: graph.nodes.filter((node) => scope === void 0 || node.plugin.scope === scope).map((node) => ({
55
67
  id: node.entry.id,
@@ -1,9 +1,9 @@
1
- import { Context, Effect, Exit, Ref, Scope } from "effect";
1
+ import { Context, Effect, Exit, Ref, Scope, Semaphore } from "effect";
2
2
  import * as Layers from "effect/Layer";
3
3
  import { HarnessError } from "@xandreed/core";
4
4
  export const makeLiveLayer = (initial) => Effect.gen(function* () {
5
- const dependencies = yield* Effect.context(), gate = yield* Effect.makeSemaphore(1), generations = yield* Ref.make(new Map), current = yield* Ref.make(0), closed = yield* Ref.make(!1), unavailable = () => new HarnessError({ code: "module.closed", message: "Module registry is closed" }), stage = (layer, id) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
6
- const scope = yield* Scope.make(), services = yield* restore(Layers.buildWithScope(layer, scope).pipe(Effect.provide(dependencies))).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void));
5
+ const dependencies = yield* Effect.context(), gate = yield* Semaphore.make(1), generations = yield* Ref.make(new Map), current = yield* Ref.make(0), closed = yield* Ref.make(!1), unavailable = () => new HarnessError({ code: "module.closed", message: "Module registry is closed" }), stage = (layer, id) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
6
+ const scope = yield* Scope.make(), memoMap = yield* Layers.makeMemoMap, services = yield* restore(Layers.buildWithMemoMap(layer, memoMap, scope).pipe(Effect.provide(dependencies))).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void));
7
7
  return { id, services, scope, leases: 0, retired: !1 };
8
8
  })), first = yield* stage(initial, 0);
9
9
  yield* Ref.set(generations, new Map([[0, first]]));
package/dist/plugins.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Effect, JSONSchema } from "effect";
1
+ import { Effect } from "effect";
2
2
  import { HarnessError } from "@xandreed/core";
3
3
  import type { HarnessConfig, Plugin } from "@xandreed/core";
4
4
  export declare const loadPlugins: (config: HarnessConfig, builtins: ReadonlyArray<Plugin>, workspace: string, home: string) => Effect.Effect<ReadonlyArray<Plugin>, HarnessError>;
5
- export declare const pluginSchema: (plugin: Plugin) => JSONSchema.JsonSchema7Root;
5
+ /** A plugin's options as one JSON Schema, its definitions inline under `$defs`. */
6
+ export declare const pluginSchema: (plugin: Plugin) => import("effect/JsonSchema").JsonSchema;
package/dist/plugins.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { join, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import { Effect, JSONSchema } from "effect";
4
+ import { Effect, Schema } from "effect";
5
5
  import { HarnessError } from "@xandreed/core";
6
- export const loadPlugins = (config, builtins, workspace, home) => Effect.reduce([...new Set((config.plugins ?? []).filter((entry) => entry.enabled !== !1).map((entry) => entry.use))], builtins, (loaded, use) => {
6
+ export const loadPlugins = (config, builtins, workspace, home) => Effect.reduce([...new Set((config.plugins ?? []).filter((entry) => entry.enabled !== !1).map((entry) => entry.use))], () => builtins, (loaded, use) => {
7
7
  if (loaded.some((plugin) => plugin.id === use))
8
8
  return Effect.succeed(loaded);
9
- return (use.startsWith(".") || use.startsWith("/") ? Effect.succeed(resolve(workspace, use)) : Effect.try(() => createRequire(join(workspace, "package.json")).resolve(use)).pipe(Effect.orElse(() => Effect.try(() => createRequire(join(home, ".efferent/plugins/package.json")).resolve(use))))).pipe(Effect.flatMap((path) => Effect.tryPromise(() => import(pathToFileURL(path).href))), Effect.map((module) => {
9
+ return (use.startsWith(".") || use.startsWith("/") ? Effect.succeed(resolve(workspace, use)) : Effect.try(() => createRequire(join(workspace, "package.json")).resolve(use)).pipe(Effect.catch(() => Effect.try(() => createRequire(join(home, ".efferent/plugins/package.json")).resolve(use))))).pipe(Effect.flatMap((path) => Effect.tryPromise(() => import(pathToFileURL(path).href))), Effect.map((module) => {
10
10
  const value = module.default;
11
11
  if (value === null || typeof value !== "object" || typeof value.build !== "function" || !Array.isArray(value.requires) || !Array.isArray(value.provides) || typeof value.version !== "string")
12
12
  return { valid: !1 };
13
13
  return { valid: !0, plugin: { ...value, id: use } };
14
14
  }), Effect.mapError((error) => new HarnessError({ code: "plugin.load", plugin: use, message: `Cannot load installed plugin ${use}: ${String(error)}` })), Effect.flatMap((result) => result.valid ? Effect.succeed([...loaded, result.plugin]) : Effect.fail(new HarnessError({ code: "plugin.manifest", plugin: use, message: "The module must default-export a definePlugin definition" }))));
15
- }), pluginSchema = (plugin) => JSONSchema.make(plugin.schema);
15
+ }), pluginSchema = (plugin) => {
16
+ const document = Schema.toJsonSchemaDocument(plugin.schema);
17
+ return Object.keys(document.definitions).length > 0 ? { ...document.schema, $defs: document.definitions } : document.schema;
18
+ };
@@ -1,5 +1,5 @@
1
1
  import { Context } from "effect";
2
- declare const Version_base: Context.TagClass<Version, "test/Version", {
2
+ declare const Version_base: Context.ServiceClass<Version, "test/Version", {
3
3
  readonly value: number;
4
4
  }>;
5
5
  export declare class Version extends Version_base {
@@ -1,4 +1,4 @@
1
1
  import { Context } from "effect";
2
2
 
3
- export class Version extends Context.Tag("test/Version")() {
3
+ export class Version extends Context.Service()("test/Version") {
4
4
  }
@@ -1,8 +1,11 @@
1
1
  import { Context } from "effect";
2
- declare const Value_base: Context.TagClass<Value, "test/Value", number>;
2
+ declare const Value_base: Context.ServiceClass<Value, "test/Value", number>;
3
3
  export declare class Value extends Value_base {
4
4
  }
5
- declare const Consumer_base: Context.TagClass<Consumer, "test/Consumer", number>;
5
+ declare const Consumer_base: Context.ServiceClass<Consumer, "test/Consumer", number>;
6
6
  export declare class Consumer extends Consumer_base {
7
7
  }
8
+ declare const Items_base: Context.ServiceClass<Items, "test/Items", readonly string[]>;
9
+ export declare class Items extends Items_base {
10
+ }
8
11
  export {};
@@ -1,7 +1,10 @@
1
1
  import { Context } from "effect";
2
2
 
3
- export class Value extends Context.Tag("test/Value")() {
3
+ export class Value extends Context.Service()("test/Value") {
4
4
  }
5
5
 
6
- export class Consumer extends Context.Tag("test/Consumer")() {
6
+ export class Consumer extends Context.Service()("test/Consumer") {
7
+ }
8
+
9
+ export class Items extends Context.Service()("test/Items") {
7
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xandreed/runtime",
3
- "version": "0.3.0",
3
+ "version": "0.8.0-next.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -10,8 +10,8 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
- "@xandreed/core": "0.3.0",
14
- "effect": "3.21.4"
13
+ "@xandreed/core": "0.8.0-next.0",
14
+ "effect": "4.0.0-rc.118"
15
15
  },
16
16
  "private": false,
17
17
  "types": "./dist/index.d.ts",